Content site architecture
Content site architecture: the decisions that matter
The architecture of a content site comes down to 4 choices: structured JSON documents, not stored HTML, static/ISR rendering with on-demand revalidation behind the edge cache, teaser in cached HTML, gated body from an authed no-store endpoint and pre-generate image variants at upload; no per-request transforms. Each is explained below with the alternatives that were rejected and what you accept by choosing it.
Last checked
Structured JSON documents, not stored HTML
Situation: Content outlives designs: the same article must render on web, RSS, AMP-like views, email and search, and user-authored HTML is the largest XSS surface in a CMS.
Choice: Store a versioned block document (ProseMirror-style JSON) as the source of truth, derive HTML, plain text and excerpts on save with an allow-list serializer.
- Rejected: Store sanitized HTML: simple but hard to migrate, restyle or render to email and feeds.
- Rejected: Markdown only: portable but poor for rich blocks and embeds.
- Rejected: Third-party headless CMS: fast to start but per-seat and per-API-call pricing and data lock-in.
- Consequence: A schema version and migrations for documents are required.
- Consequence: Rendering is deterministic and sanitizable in one place.
- Consequence: Search and RSS use the derived plain text and excerpt.
Static/ISR rendering with on-demand revalidation behind the edge cache
Situation: A viral post can bring 100x normal traffic in minutes; rendering each request against Postgres is slow and expensive, yet editors expect changes to appear immediately.
Choice: Generate public pages statically, cache them at the edge with stale-while-revalidate, and revalidate by tag and path (and purge the edge) at publish, edit, unpublish and slug change.
- Rejected: Server-render every request: fresh but database load scales with readers.
- Rejected: Fully static export rebuilt on every change: fast reads but slow publish at thousands of pages.
- Rejected: Long TTLs without revalidation: cheap but stale corrections and unpublishes.
- Consequence: Readers rarely touch Postgres, so traffic spikes cost almost nothing.
- Consequence: Revalidation fan-out (home, tag, collection, feed, sitemap) must be complete and is tested.
- Consequence: Personalised UI is fetched client-side from private endpoints.
Teaser in cached HTML, gated body from an authed no-store endpoint
Situation: Static caching and paywalls conflict: if gated text is in cached HTML, one cache hit leaks paid content to everyone and crawlers.
Choice: Cached pages contain only the teaser and a placeholder; after canRead the client fetches the full body from a private, no-store endpoint; feeds, sitemaps, OG and search snippets use the excerpt only.
- Rejected: Hide gated text with CSS or client JS only: trivially bypassed with view-source.
- Rejected: Server-render gated pages per reader: correct but loses edge caching for the most valuable pages.
- Rejected: Cookie-vary caching: fragile and easy to mis-key.
- Consequence: Paid readers see a short loading step for the body.
- Consequence: Crawlers index the teaser unless Paywalled-content markup is configured.
- Consequence: Entitlement correctness depends on webhook-written memberships, tested end to end.
Pre-generate image variants at upload; no per-request transforms
Situation: Images are most of the bytes; resizing on demand is flexible but bills per transformation and adds latency and abuse risk.
Choice: A sandboxed batch job generates a fixed set of AVIF/WebP/JPEG widths under immutable keys once, served from the edge, with stored dimensions and blurhash.
- Rejected: On-demand resizing service: flexible but a per-use bill that traffic or attackers can inflate.
- Rejected: Serve originals: huge egress and failing Core Web Vitals.
- Rejected: Client-side resizing only: inconsistent quality and no bandwidth savings.
- Consequence: Storage grows by a bounded multiple per image.
- Consequence: New breakpoints require a backfill job under a processing budget.
- Consequence: Hot-linking cost is bounded by egress caps.
How the data is indexed
Indexes are chosen from the questions the app asks most often. Each line below names a table and the question its main index answers.
- author_profiles: unique (slug) — author page URL.
- collections: unique (parent_id, slug) — sibling slugs are unique inside one parent.
- posts: unique (kind, slug) — one URL per slug and kind, lookup by slug on every page render.
- post_revisions: unique (post_id, rev) — revision numbers are unique, saves are idempotent.
- redirects: unique (from_path) — one redirect per old path, lookup on 404 and in middleware cache.
- tags: unique (slug) — tag page lookup.
- post_tags: (tag_id, post_id) — tag page: posts for a tag.
- media_assets: (owner_id, created_at DESC) — media library keyset page and storage quota.
What Forge64 provides underneath
Forge64 matches your description to one of 25 reference architectures, writes the plan and data model into the project, generates the app, and runs a quality gate over the code before you see it. Each app gets its own Postgres database and sign-in with no per-user fee. Hosting on Forge Cloud and Forge Payments are rolling out.
This is a reference design for the category, not a promise that every item is generated today. Built-ins such as background jobs, file uploads and search are rolling out; email sent from your app is coming soon, so anything here that needs it is something you add yourself for now.
FAQ
- What is the most important architecture decision for a content site?
- Structured JSON documents, not stored HTML. Store a versioned block document (ProseMirror-style JSON) as the source of truth, derive HTML, plain text and excerpts on save with an allow-list serializer.
- Do I have to make these decisions myself?
- No. Forge AI plans them from your description and writes the decisions into the project as architecture records you can read and change.
- What do the alternatives cost me?
- For example, for "Static/ISR rendering with on-demand revalidation behind the edge cache": Server-render every request: fresh but database load scales with readers. Fully static export rebuilt on every change: fast reads but slow publish at thousands of pages.
Build your app with Forge64
One prompt to a running app with auth, database, hosting and payments. Start free, no credit card required.
Start building for free