Octa Engineering Notes Christian Elías Request for Comments: OCTA-8 octa.page Category: Informational May 2026 RSS as a cross-site content bridge in Astro SSG Abstract Using RSS as a stateless content contract between two independent Astro static sites, with GitHub Actions repository_dispatch to trigger rebuilds only when Markdown content changes — not on every commit. Status of This Memo This memo documents architecture notes for the Internet community. It does not specify an Internet standard of any kind. Distribution of this memo is unlimited. The canonical version lives at https://octa.page/doc/rss-as-cross-site-content-bridge/ and the machine-readable source at https://octa.page/doc/rss-as-cross-site-content-bridge.md Two static sites. One is a portfolio (christianecg.com [1]), built with Astro and deployed to GitHub Pages. The other is this notebook (octa.page [2]), where active writing happens. The portfolio had a "/blog" section — but all the articles in it were from 2021–2022. The writing had moved to octa.page and the two were completely siloed. The technical debt: a blog section showing stale content while the active equivalent existed elsewhere and neither site knew about the other. 1. Why RSS and not something else The options were roughly: 1. Shared CMS or database — overkill for two static sites. Introduces infrastructure neither site needs. 2. Git submodule or monorepo — couples the two repos, complicates deploys. 3. Direct fetch from the octa.page source files — would require shared repo access and tight coupling. 4. RSS — octa.page already exposes "/rss.xml". It's a stable contract. Stateless, versionless, requires nothing from the consumer. RSS wins by elimination. It's also the right abstraction: octa.page doesn't need to know christianecg.com exists. The feed is an implementation detail of octa's public interface, consumed unilaterally. 2. Build-time consumption in Astro Astro SSG runs "fetch" at build time in the page's frontmatter. No runtime, no hydration — the HTML is generated once with whatever the feed returns at that moment. let octaArticles: Article[] = []; try { const res = await fetch('https://octa.page/rss.xml'); const xml = await res.text(); octaArticles = [...xml.matchAll(/([\s\S]*?)<\/item>/g)].flatMap(([, item], i) => { const title = item.match(/(.*?)<\/title>/)?.[1]?.trim() ?? ''; const link = item.match(/<link>(.*?)<\/link>/)?.[1]?.trim() ?? ''; const pub = item.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() ?? ''; const tags = [...item.matchAll(/<category>(.*?)<\/category>/g)].map(m => m[1]); const date = pub ? new Date(pub).toISOString().split('T')[0] : ''; if (!title || !date) return []; return [{ id: `octa-${i}`, title, date, excerpt: '', tags, source: 'Octa', url: link, external: true, idx: 0 }]; }); } catch { // RSS fetch failed — show only local articles } A few constraints drive the implementation: No DOMParser in Node.js. Browser APIs aren't available in the build context. Regex over the raw XML string is the pragmatic choice for a feed this simple. A proper XML parser ("fast-xml-parser" etc.) would be justified if the feed had CDATA sections or nested namespaces — this one doesn't. Graceful degradation is non-negotiable. A failed "fetch" during build shouldn't break the site. The "try/catch" ensures the page builds with only local articles if the feed is unreachable. Merging and sorting happen in-process. Local articles and RSS articles are normalized to the same "Article" type, merged into a single array, and sorted by date. The "external" flag drives rendering decisions (badge, "target="_blank"", "↗" vs "→"). 3. The freshness problem Build-time consumption means the portfolio goes stale as soon as octa.page publishes something new. The obvious mitigation — a scheduled nightly rebuild — has the wrong model: it rebuilds christianecg.com on a fixed interval regardless of whether octa.page changed. The correct trigger is octa.page's own deploy completing. 4. Cross-repo dispatch GitHub Actions supports "repository_dispatch": an HTTP event that triggers a workflow in another repo. The pattern: octa.page dispatches after deploy: trigger-christianecg: needs: [deploy, check-content] if: needs.check-content.outputs.md_changed == 'true' runs-on: ubuntu-latest steps: - name: Trigger christianecg.com rebuild run: | curl -s -X POST \ -H "Authorization: token ${{ secrets.CHRISTIANECG_DEPLOY_TOKEN }}" \ -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/ChristianECG/christianecg.com/dispatches \ -d '{"event_type":"octa-deployed"}' christianecg.com listens for the event: on: push: branches: [main] workflow_dispatch: repository_dispatch: types: [octa-deployed] The "if: needs.check-content.outputs.md_changed == 'true'" condition is the key constraint. A style change, config update, or tooling commit on octa.page still triggers a full build and deploy of octa — but doesn't cascade to christianecg.com. The dispatch only fires when ".md" files are in the diff. The "check-content" job detects this: check-content: runs-on: ubuntu-latest outputs: md_changed: ${{ steps.check.outputs.md_changed }} steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - id: check run: | if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '\.md$'; then echo "md_changed=true" >> $GITHUB_OUTPUT else echo "md_changed=false" >> $GITHUB_OUTPUT fi "fetch-depth: 2" gives access to "HEAD~1". Without it, a shallow clone has no parent to diff against. 5. Trade-offs | | | |---|---| | **Freshness** | Near-zero delay when dispatch works. Falls back to manual `workflow_dispatch` if the PAT expires or the hook fails. | | **Coupling** | One-directional. octa.page doesn't know christianecg.com exists. christianecg.com depends on the RSS contract, which is stable. | | **Failure modes** | RSS unreachable at build time → local-only blog, no build failure. Dispatch token expired → stale portfolio until manual rebuild. | | **Complexity added** | One PAT, one secret, ~20 lines of YAML across two repos. | The RSS contract is the right boundary. It decouples publication from consumption, keeps both sites independently deployable, and adds no shared infrastructure. 6. References [1] https://christianecg.com [2] https://octa.page Christian Elías Informational [Page 1]