Here is the useful conclusion first: a small static knowledge site can have a visual editor, draft previews, automated checks, and rollback without adding a VPS, a database, or a second content source.
The owner and Codex built this path together. The owner defined the site goals, cost boundary, and publication rules; Codex assisted with configuration, testing, and evidence organization. This article treats only behavior supported by the repository and build results as fact.
The site’s day-to-day convention is deliberately narrow: Pages CMS targets GitHub main for routine saves, while isolated tests may save to non-production branches. Cloudflare Pages builds non-production previews. GitHub Actions validates the exact commit requested for release. In the current controlled workflow, validation must pass before an ordinary push fast-forwards production to the specified main commit. Cloudflare Pages then publishes that branch to the live domain.
This design does not require a separate VPS or content database for the CMS, but it is not a “zero-cost” claim. GitHub Actions is subject to account allowances, Cloudflare Pages has plan and usage limits, and the domain retains its renewal cost. The point is one fewer self-hosted service, not a verified bill or a promise of permanently free infrastructure.
The missing layer was editing, not another content system
The site already had a reliable core: Astro reads Markdown from the repository, produces static pages, and Cloudflare Pages serves them. The maintenance problem was more specific. Editing an entry meant working directly with frontmatter, filenames, media paths, and Git.
A database-backed CMS, a WordPress installation, or another self-hosted CMS could provide a larger feature set. It would also create another set of responsibilities: runtime updates, backups, permissions, migrations, monitoring, and a decision about whether the repository or the CMS owns the canonical content.
Those responsibilities were not justified by the current site. Pages CMS takes a narrower role: it adds an editor on top of the GitHub repository, while Markdown and media remain the content source of truth. That follows the same constraint described in Why Build the Knowledge Site Before the Full Knowledge System: add infrastructure after real use exposes a need for it, not before.
Five stages with separate responsibilities
The whole pipeline fits in one diagram:

Edit in Pages CMS
→ save to GitHub main
→ build a Cloudflare Preview
→ validate with GitHub Actions
→ fast-forward production and deploy with Cloudflare Pages
The tools are ordinary. The value comes from keeping their responsibilities separate.
1. Pages CMS edits repository files
The root .pages.yml defines the Chinese and English collections, allowed image formats, translation pairing key, and default draft state. Editors can create entries, but rename and delete are disabled. settings.content.merge: true preserves frontmatter keys that are not exposed by the editor schema.
Saving in Pages CMS creates a Git commit. It does not decide whether that commit belongs on the public site.
2. main stores day-to-day edits
Routine content is configured to save to main; Pages CMS does not technically force every save in every context to use that branch. This convention provides a diff, a durable commit history, and a Cloudflare preview without changing the production domain.
This split removes a dangerous assumption: saving and publishing are not the same event. A title, translation pair, mobile layout, and set of links can be reviewed before the edit affects production.
3. Preview renders drafts and sends anti-indexing directives
Non-production builds can render draft: true entries and display a visible draft notice. Every preview page carries noindex, nofollow; its robots.txt disallows crawling, while canonical URLs continue to use the production host. These directives are intended to reduce search exposure, not to provide access control.
Whether a preview is public depends on its Cloudflare Access configuration. Without Access, anyone who knows the URL may be able to open it. With or without Access, noindex is an indexing directive rather than access control. Google also has to crawl a page before it can read the page-level rule; if robots.txt blocks that crawl, Google may never see it.
4. GitHub Actions validates before release
The “Publish validated main to production” action in Pages CMS triggers a workflow_dispatch job. The workflow checks the source, action, repository, and ref fields declared by the payload, confirms that the requested SHA is the current main commit, and checks that production is still an ancestor of main. This is input-contract validation, not independent cryptographic authentication of the person who triggered it.
Only then does it run:
npm ci
npm audit --audit-level=moderate
npm run verify
The repository’s verification command covers the content schema, bilingual pairing, Astro diagnostics, unit tests, the production build, draft exclusion, canonical and hreflang relationships, sitemap and RSS output, internal links, and licenses. A failure leaves production unchanged.
5. The current workflow performs a normal fast-forward
After validation, the current controlled workflow uses an ordinary push to fast-forward production to the exact main commit. It does not use force push. If the branches have diverged, this workflow stops so the divergence can be understood first. This review did not establish a GitHub ruleset or branch-protection rule that blocks every other write, so the workflow constraint should not be confused with an absolute branch-level restriction.
Cloudflare Pages treats production as the production branch. A routine CMS save can create a preview, but it cannot cross the validation boundary and update lab.omnihex.xyz on its own.
Draft isolation is more than one flag
The pipeline does not rely on a single checkbox in the editor.
Production content queries require draft === false. Static route generation excludes drafts. The home page, archive, and RSS feed all use the same published-content query. The sitemap filters draft URLs, and a draft detail page never emits BlogPosting JSON-LD in preview.
After the build, scripts/verify-dist.mjs scans the generated HTML, XML, and text files to ensure that no draft route or slug remains. The publication rule is checked once while selecting content and again against the actual output.
One fewer service does not mean zero cost
This comparison asks whether the implementation needs another self-hosted runtime. It does not audit the account’s actual bill.
| Layer | Fixed increase for this project | Cost or limit that still matters |
|---|---|---|
| Hosted Pages CMS editor | No self-hosted CMS runtime or content database | Hosted availability, GitHub App permissions, and future product changes |
| Private GitHub repository and Actions | Reuses the existing Git content source | Private-repository Actions can incur usage charges beyond the plan allowance |
| Cloudflare Pages static hosting | No separate content application server | Plan and usage limits still apply to builds, projects, and files |
| Domain | Uses an existing omnihex.xyz subdomain |
The underlying domain still has its existing renewal cost |
Human maintenance is also real. Someone still reviews claims, reads diffs, investigates failed builds, and decides when to release. The system removes an extra server and a content synchronization problem; it does not remove editorial responsibility.
How the pipeline was verified
The evidence is a set of checkpoints rather than an architecture diagram:
- the four entries that existed when the CMS was integrated use an explicit Boolean
draft: false, while new entries default todraft: true; - temporary Chinese and English drafts were created, paired, previewed with a test image, and removed on an isolated branch;
- candidate branches run both the production build and preview-isolation checks in GitHub Actions;
- the current release workflow fast-forwards the specified
maincommit only after its gates pass; - later candidates added the SEO gate and live-domain checks;
- the live test checks sitemap routes, the 404 page, verification metadata, and the permanent
pages.devredirect.
These checks show that the current rules work. They cannot prove that every future edit will be correct, so rollback remains part of the design. A Cloudflare rollback can first switch production traffic to a previous successful deployment, but it does not change Git branches. After an emergency rollback, a git revert or corrective commit is still required to bring repository history and the deployed state back into alignment.
When this approach fits—and when it does not
This pipeline fits a file-based site with a moderate publishing rate, a small editorial group, and a reason to keep Git history and static deployment. It is particularly useful when an Astro, Hugo, or Jekyll site already works and the missing piece is an approachable editor.
It is a poor fit when the product needs complex roles, simultaneous editing, forms, comments, payments, personalized queries, or a complex data model with many relationships. In those cases, a database and an application backend may be requirements rather than overengineering.
The conclusion is not that every site should use this stack. It is that a visual editor should not automatically force a small static site to adopt a server. Start with the shortest path that preserves diffs, blocks failed releases, and keeps a rollback target. Expand it only when observed friction justifies the next layer.
That approach also supports the broader lab goal: turning real project work into reusable knowledge without hiding the failed attempts or the conditions under which a decision remains valid.