The hub idea

open.awareride.com is one rendered surface for content that lives in many repos. The blog you are reading is authored in a separate Posts repo. Packscope’s docs are authored in the packscope repo. The hub - awareride/awareride.github.io - just builds and renders them.

The question was how to move content from N external repos into the hub’s src/content/ without turning the hub into a merge conflict zone, and without ever letting an external push land on main unreviewed.

The contract

Each external repo mirrors the hub’s locale layout and lets a GitHub Action copy its posts/ and docs/ in:

External repo Hub
posts/ src/content/posts/
docs/ src/content/docs/${PRODUCT}/

The ${PRODUCT} segment is the interesting part. An external repo’s docs are flat by locale - docs/en/getting-started.md, not docs/packscope/en/getting-started.md. The product is injected at sync time from a PRODUCT env var in the workflow. That keeps relative Markdown links working when you browse the repo on GitHub: they resolve against docs/, not docs/packscope/.

Sync opens a PR, never pushes to main

The sync workflow does not push to main. It clones the hub, copies content with rsync, commits to a dedicated branch, and opens a pull request:

rsync -a posts/ "$CLONE_DIR/src/content/posts/"
git checkout -b "sync-posts-${SRC}-${RUN_ID}"
git commit -m "posts: sync from ${SOURCE_REPO}"
git push -u origin "$BRANCH"
gh pr create --repo awareride/awareride.github.io --base main --head "$BRANCH" \
  --title "posts: sync from ${SOURCE_REPO}"

rsync -a posts/ dest/ (note the trailing slash) means “the contents of posts/”, so it never creates posts/posts/ regardless of whether the destination already exists - a bug that bit me with a cp -R based action earlier. A human reviews the PR and merges. Nothing lands unattended.

Auth is a fine-grained PAT stored as DOCS_CENTRAL_HUB_TOKEN in the external repo, with Contents and Pull-requests write on the hub.

The skill, not copy-paste

Rather than ask every external repo to reinvent this, the whole thing is packaged as a skill: awareride-content-sync. An external repo copies it into .agents/skills/awareride-content-sync/ and gets the workflow templates, a validation script, and the docs in one drop.

The validator runs before sync and gates it - pure Node stdlib, no dependencies:

node .agents/skills/awareride-content-sync/scripts/validate.mjs

It enforces frontmatter conformance and the slug contract: a file’s slug is its path relative to the locale dir, and it must be byte-identical across locales. en/getting-started.md pairs with zh/getting-started.md - never zh/Getting-Started.md - or per-page fallback breaks.

Retiring content

The rsync copy only adds and overwrites; it never deletes, so content from one project can’t wipe another project’s pages. That means removing a file locally does not remove it from the hub.

The opt-in answer is sync-delete.list at the repo root - one path per line, a trailing slash for a whole directory. After the copy, apply-delete-list.mjs removes exactly those paths, and the deletion ships in the same reviewable PR as the additions:

posts/en/old-post.md
docs/en/legacy/

Source Markdown keeps GitHub-friendly relative links - ./getting-started.md, ../zh/architecture.md. On the hub, Astro’s Markdown processor does not rewrite body links, so a raw ./getting-started.md would render as href="./getting-started.md" and 404. A small Sätteri mdast plugin fixes that at build time: it scans src/content/**/*.md once, builds a map of file paths to site URLs, and rewrites relative .md links to their hub routes - ./getting-started.md becomes /packscope/docs/getting-started. No source edits, links work on GitHub and on the site.

What it adds up to

An external repo writes Markdown in a flat, GitHub-readable layout. On push to main, a workflow validates it, copies it into the hub on a branch, and opens a PR. The hub reviews, merges, and a manual deploy ships it. The content lives where it is maintained; the hub only renders.


Part of the series on building open.awareride.com. Previous: two locales with per-page fallback. Start at the beginning: Rebuilding open.awareride.com on Astro.