Content sync
This guide explains how content from a separate repository gets into the
hub. It mirrors the awareride-content-sync skill shipped in this repo (under
.agents/skills/awareride-content-sync/). To write content directly in the hub
instead, see Authoring.
The hub itself — and the documentation you are reading — follows this exact
model. The docs/ folder in this repository is synced into the hub as the
astro-content-hub product.
The model
The hub aggregates content from many source repos. An external repo authors
posts/ or docs/ in a locale layout; a GitHub Action validates it and opens
a pull request against the hub’s main. A human reviews the PR; when
merged, the hub builds and deploys. Nothing lands on the hub’s main
directly — content is reviewed first.
The layout (mirror the hub’s locale dimension)
<external-project>/
posts/
en/hello-world.md <- /posts/hello-world/ on the hub
zh-Hans/hello-world.md <- SAME filename as en/ (slug contract)
en/my-product/foo.md <- nested dirs become path segments
docs/
en/index.md <- product docs landing page
en/getting-started.md
zh-Hans/index.md <- optional; falls back to en if absent
.agents/skills/awareride-content-sync/ <- the sync skill (copied in)
sync-delete.list <- opt-in deletion manifest (see below)
posts/ maps onto the hub’s src/content/posts/; docs/ maps onto
src/content/docs/${PRODUCT}/. The product segment is added by sync from the
PRODUCT env var, not present in the external repo — this keeps relative
markdown links resolving against docs/ on GitHub.
Interactivity syncs too. Because the copy is a straight file copy, any
inline <script> inside your Markdown ships with it — so a button, tab, or
chart written in your repo’s docs/en/foo.md is interactive on the hub with
zero extra setup. Follow the authoring guidance
(self-contained, no external requests) and it passes review as-is.
Frontmatter schemas
Posts (posts/<locale>/<slug>.md):
---
title: "Post Title" # required
date: 2025-07-21 # required, YYYY-MM-DD
description: "One-line summary." # required
tags: ["announcement"] # optional
author: "Your Name" # optional
source: "https://github.com/owner/repo"# optional
draft: false # optional; excluded from the hub
---
Docs (docs/<locale>/<slug>.md):
---
title: "Page Title" # required
description: "Short summary" # optional
order: 2 # optional, sidebar sort (default 0)
---
Docs have no date, tags, author, or draft.
Slug contract
A file’s slug is its path relative to the locale dir, without .md. The slug
must be byte-identical across locales so fallback works (en/foo.md and
zh-Hans/foo.md both have slug foo). Always write the en version first.
Fallback
Fallback is per-page and content-level, never a redirect. A missing zh-Hans page
renders the en body inside a zh-Hans shell with a notice; post cards on
/zh-Hans/posts/ show an EN badge. Ship en first and translate incrementally —
the site never 404s on a missing translation.
Internal links
- In an
enpost/doc, link with default paths:/posts/foo/,/<product>/docs/bar/. - In a
zh-Hanspost/doc, use the/zh-Hans/prefix to keep readers in the Chinese shell:/zh-Hans/posts/foo/,/zh-Hans/<product>/docs/bar/.
Local validation
A zero-dependency Node script checks frontmatter and the slug contract:
node .agents/skills/awareride-content-sync/scripts/validate.mjs
It exits non-zero on any error, so it can gate the sync workflow. It catches
missing/invalid frontmatter, zh-Hans files with no matching en file, and a
missing en/ locale dir. Run it whenever you add or rename content files.
Syncing to the hub
- Create the PAT (one-time, on the hub side). Create a fine-grained PAT on
the hub repo with Contents: write and Pull requests: write. Add it
as a repository secret named
DOCS_CENTRAL_HUB_TOKENin the external repo. - Add the workflow. Copy
sync-docs.yml(orsync-posts.yml) from.agents/skills/awareride-content-sync/templates/into.github/workflows/. For docs, setPRODUCTto your product name. Both validate first, then open a PR. - Directory mapping.
posts/→src/content/posts/,docs/→src/content/docs/${PRODUCT}/.
The copy is a merge, not a mirror: it adds/overwrites the external repo’s files in the hub and leaves other projects’ content untouched.
Deleting content (sync-delete.list)
The merge copy never deletes hub-only files. To retire a page, list it in
sync-delete.list at the repo root:
# one path per line, relative to the repo root; '#' and blank lines ignored
posts/en/old-post.md
posts/zh-Hans/old-post.md
docs/en/legacy/ # trailing slash = drop the whole directory
- Paths map through the copy (
posts/...→src/content/posts/...,docs/...→src/content/docs/${PRODUCT}/...). - The
sync-postsworkflow only processesposts/...lines;sync-docsonlydocs/...lines. - A trailing slash removes a directory. Unsafe paths (
.., absolute, or the bare collection root) are rejected. - Deletions and additions land in the same reviewable PR.
Registering a new product (docs only)
Docs only render on the hub if the product is registered in the hub’s
products array (site.config.ts at the repo root). This is a one-time hub-side change an
owner makes via PR — the external repo cannot do it through sync. Once merged,
set PRODUCT in your sync-docs.yml. Posts need no registration.
What can break the hub build
The hub runs npm run build (Astro + astro check, zero errors expected).
Your content can break it via: mismatched frontmatter types, duplicate slugs
within a locale, a zh-Hans-only slug with no en file, or internal links to
non-existent pages. validate.mjs catches most of these; run it before
pushing.