From zero to portal: organization-scale document publishing with Metanorma
What you will build
Push a document to main in any repository. It is compiled, released as a
GitHub Release with metadata, and automatically appears on the right page of
the organization portal. No submodules, no hardcoded repo lists, no manual
updates.
Author pushes to main
│
▼
Per-document repo CI
compile → route via metanorma.release.yml → release (GitHub Release with channel metadata)
│
▼
Portal CI (scheduled or triggered)
discover repos → fetch releases → subscribe by channel → extract → build site
│
▼
Live portal (standards.calconnect.org)
This post walks through every file you need to create, using real examples from CalConnect’s standards portal — which aggregates 168 documents from 51 repositories across 5 channels.
For the GitHub Actions reference (action inputs, metadata protocol), see GitHub Actions integration. For a permanent setup reference with full config schemas, see Organization-Scale Publishing Setup.
Prerequisites
-
A GitHub organization with one or more document repositories
-
A Metanorma flavor installed (e.g.,
metanorma-ccfor CalConnect) -
The
ghCLI for adding GitHub topics -
Basic familiarity with GitHub Actions
The two-file architecture
The entire system is driven by two config files — one per document repo, one for the portal:
- Per-repo
metanorma.release.yml— routing. -
The document repository declares which channels each document is published to. Pattern matching (
documents[].pattern), stage/doctype matching (documents[].stage,documents[].doctype), and a catch-all default entry all live here. The repo owns its routing decisions. No.metanorma/directory is needed. - Aggregator
metanorma.aggregate.yml— discovery and display. -
The portal repository subscribes to channels by prefix, defines display categories for site rendering, and configures output layout. It does not tell document repos where to publish — it only decides what it collects.
Channels are a publisher-side concept: the document repo picks channel
names, the aggregator subscribes by prefix. It is convention, not a central
registry. There is no org config repo, no shared .metanorma/ directory, and
no org: key.
Step 1: Set up a single-document repository
CalConnect has ~45 standard repositories, each containing one document.
cc-datetime-explicit is a typical example — it produces
CC 18011:2018 "Date and time — Explicit representation".
Three files are needed (no .metanorma/ directory required):
metanorma.yml (compilation manifest)
# cc-datetime-explicit/metanorma.yml
metanorma:
source:
files:
- sources/cc-18011.adoc
collection:
name: TC-DATETIME
organization: CalConnect
This tells metanorma site generate which source file to compile. See
site generation docs for the full schema.
metanorma.release.yml (release manifest and routing)
# cc-datetime-explicit/metanorma.release.yml
documents:
- pattern: "cc-*"
channels: [public/standards]
One line of routing: any document with an ID matching the cc-* glob goes to
the public/standards channel. When an author adds a new document matching the
pattern, it is automatically included — no manifest update needed.
The release manifest is the single point of truth for routing. It uses a
unified documents list where each entry can match by pattern (glob against
document ID), stage (ISO stage code), doctype (document type), or any
combination. Entries are evaluated in order; first match wins. A catch-all
entry (just channels:) serves as the default.
.github/workflows/release.yml
# cc-datetime-explicit/.github/workflows/release.yml
name: Release
on:
push:
branches: [main]
paths: ['sources/**', 'metanorma.yml', 'metanorma.release.yml']
workflow_dispatch:
permissions:
contents: write
jobs:
release:
uses: actions-mn/.github/.github/workflows/metanorma-release.yml@main
with:
default-visibility: private
secrets: inherit
That is the entire release workflow — 10 lines. The reusable workflow handles compilation, change detection (via content hash), packaging, and publishing to GitHub Releases.
Add the metanorma-release topic
gh api repos/CalConnect/cc-datetime-explicit/topics -X PUT \
--field names='["metanorma-release"]'
This is how the portal discovers your repository. No topic, no aggregation.
What happens when you push
-
The reusable workflow runs
metanorma site generateto compile the AsciiDoc source into HTML, PDF, XML, and RXL. -
It parses the RXL file to extract document metadata (ID, title, stage, edition).
-
It matches
cc-18011against thepattern: "cc-*"rule and resolves the channel topublic/standards. -
It computes a content hash. If the hash matches the previous release, it skips (no-op). If different, it proceeds.
-
It creates a GitHub Release with tag
cc-18011-2018/ed1, attaches a zip containing all compiled files, and writes the release body with metadata:
content-hash: abc123...
<!-- mn-release-metadata
{
"version": 1,
"id": "cc-18011",
"title": "Date and time — Explicit representation",
"edition": "1",
"stage": "published",
"doctype": "standard",
"channels": ["public/standards"],
"formats": ["html", "pdf", "xml", "rxl"]
}
-->
Published releases are immutable — the tag is created once and never overwritten. Draft releases are rolling — the same tag is updated in-place as the draft evolves.
Step 2: Set up a multi-document repository
CalConnect’s cc-admin-documents repo contains 100+ documents across four
types. It demonstrates how pattern-based routing scales to many documents
with different channels.
metanorma.release.yml (four channels)
# cc-admin-documents/metanorma.release.yml
documents:
- pattern: "cc-s-*"
channels: [public/standards]
- pattern: "cc-r-*"
channels: [public/reports]
- pattern: "cc-a-*"
channels: [public/admin]
- pattern: "cc-adv-*"
channels: [public/advisories]
Four patterns, four channels. Each document is routed based on its ID convention:
-
cc-s-51015→public/standards -
cc-r-1903→public/reports -
cc-a-0001→public/admin -
cc-adv-100→public/advisories
Release workflow with filter inputs
For multi-document repos, the workflow includes optional inputs for selective re-release:
# cc-admin-documents/.github/workflows/release.yml
name: Release
on:
push:
branches: [main]
paths: ['sources/**', 'metanorma.yml', 'metanorma.release.yml']
workflow_dispatch:
inputs:
include-pattern:
description: 'Glob to filter documents (e.g. cc-1903*)'
required: false
default: '*'
force:
description: 'Force release even if unchanged'
required: false
type: boolean
default: false
jobs:
release:
uses: actions-mn/.github/.github/workflows/metanorma-release.yml@main
with:
default-visibility: private
include-pattern: ${{ github.event.inputs.include-pattern || '*' }}
force: ${{ github.event.inputs.force || 'false' }}
secrets: inherit
Directive repositories
CalConnect’s directive repos use a single channel:
# cc-directive-standardization-publication/metanorma.release.yml
documents:
- pattern: "cc-dir-*"
channels: [public/directives]
All five directive repos use the same pattern. A new directive added to any
of them is automatically routed to public/directives.
Step 3: Set up the portal
The portal is a single repository that aggregates all published documents. For CalConnect, this is standards.calconnect.org.
It needs only one config file: metanorma.aggregate.yml.
metanorma.aggregate.yml
# standards.calconnect.org/metanorma.aggregate.yml
source: github
output_dir: _site/cc
file_routing: flat
cache_dir: .cache/aggregate
data_dir: _data
channels:
- public
include_drafts: true
display_categories:
- name: Standards, Specifications & Reports
slug: standards
doctypes: [standard, specification, report]
- name: Guides & Advisories
slug: guides
doctypes: [guide, advisory]
- name: Directives
slug: directives
doctypes: [directive]
- name: Administrative
slug: administrative
doctypes: [administrative]
- name: Amendments & Technical Corrigenda
slug: amendments
doctypes: [amendment, technical-corrigendum]
github:
organizations:
- CalConnect
topic: metanorma-release
The channels: [public] entry subscribes to all channels starting with
public/ — so public/standards, public/reports, public/admin,
public/advisories, and public/directives are all included via prefix
matching. The portal does not need to know the full channel list; it
discovers it from what document repos publish.
The display_categories section maps document types (from Relaton doctype)
to human-readable category names used in site rendering. This is an
aggregator-side concern — the document repos do not need to know how the
portal categorizes them.
The include_drafts: true flag includes working drafts and committee drafts
so the portal can show a "drafts" page.
.github/workflows/build_deploy.yml
# standards.calconnect.org/.github/workflows/build_deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
workflow_dispatch:
schedule:
- cron: '0 6 * * *' # daily refresh at 06:00 UTC
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: .cache/aggregate
key: mn-aggregate-${{ github.run_id }}
restore-keys: mn-aggregate-
- name: Aggregate released documents
uses: actions-mn/aggregate@v1
with:
organizations: CalConnect
topic: metanorma-release
output-dir: _site/cc
channels: 'public'
canonicalize: true
cache-dir: .cache/aggregate
token: ${{ secrets.GITHUB_TOKEN }}
- name: Build site
run: bundle exec jekyll build
- uses: actions/upload-pages-artifact@v3
The aggregate action uses ETag-based caching. Unchanged repositories are skipped. Daily scheduled builds pick up new releases automatically.
Aggregate output
After aggregation, the output directory contains index.json with full document
metadata, a relaton/ directory with Relaton-enriched bibliography data, and
the document files themselves:
_site/cc/
index.json # Full document metadata (168 documents)
relaton/
index.json # Relaton-enriched bibliography
cc-18011-2018.html # Document files (flat routing)
cc-18011-2018.pdf
cc-18011-2018.xml
cc-18011-2018.rxl
... # 600+ files total
_data/
documents.json # Flattened data for Jekyll templates
For the full output schema and file routing options, see Aggregate output structure.
Rendering with Jekyll
Each listing page is just front matter:
# _pages/standards.html
---
layout: doc-type
title: Standards
display_category_slug: standards
---
The doc-type layout reads site.data.documents.items, filters by
display_category_slug (set by the display_categories mapping in
metanorma.aggregate.yml), and renders document cards with search, filters,
and sort — all client-side.
Step 4: Verify the pipeline
-
Check the document repo:
gh release list --repo CalConnect/cc-datetime-explicit -
Check the portal build logs for the aggregate step output (document count).
-
Visit the portal and verify the document appears on the correct page.
|
Tip
|
For troubleshooting guidance (missing releases, missing documents, stale data), see Troubleshooting in the setup reference. |
The safety model
The architecture enforces safety at three independent layers:
-
Publisher gate (manifest): Documents not matched by
metanorma.release.ymlare not released. If it is not in the manifest, it does not exist. -
Channel assignment (release action): The publisher decides the channel. The portal cannot override it.
-
Aggregation filter (aggregate action): The portal sees only the channels it asks for. A
publicportal never seesmembersorinternalchannels.
|
Important
|
Always set |
Where to go next
-
Organization-Scale Publishing Setup — permanent setup reference with full schemas, stage codes, and checklists
-
GitHub Actions integration — action inputs, release metadata protocol, force-replacing releases
-
actions-mn — GitHub Action source and issue tracker
-
standards.calconnect.org — the live portal