Skip to main content
Features

Blog & Journal: SSR Content Architecture

How ArgoBox serves 200+ blog posts and journal entries without hitting Cloudflare Worker limits — on-demand Gitea fetching, build-time metadata indexes, and marked rendering.

March 20, 2026

Blog & Journal: SSR Content Architecture

ArgoBox serves 200+ blog posts and journal entries through a hybrid SSR architecture that keeps the Cloudflare Worker bundle small while scaling to thousands of entries.

The Problem

Astro 5's content collections bundle ALL registered collection data into the SSR Worker when any route imports from astro:content. With 200+ markdown files, the data layer grows by ~100 KB+ per collection — quickly exceeding Cloudflare's 3 MiB compressed Worker limit.

The Solution

Instead of registering blog and journal collections in Astro's config.ts, ArgoBox uses a three-layer pattern:

Layer 1: Build-Time Metadata Index

scripts/build-content-index.mjs scans content directories at build time and generates src/data/content-index.json — a metadata-only index containing title, date, tags, description, and slug for each entry. At ~600 bytes per entry, 3000 posts would only be ~1.8 MB.

Layer 2: SSR Route Pages

Four SSR pages handle all content rendering:

Route Page Layout
/blog/ Blog listing with tag filtering + Tendril knowledge graph CosmicLayout
/posts/[slug]/ Individual blog post with square hero image BaseLayout
/journal/ Journal listing with pstree terminal visualization CosmicLayout
/journal/[slug]/ Journal entry with prev/next navigation CosmicLayout

All pages use export const prerender = false — they're rendered on each request, not at build time.

Layer 3: Runtime Content Fetching

When a visitor loads a blog post or journal entry:

  1. getCollectionMeta() reads the build-time metadata index (synchronous, ~0ms)
  2. readContentFile() fetches the full markdown body from Gitea API (production) or local filesystem (dev)
  3. marked converts the markdown to HTML at request time

First load takes ~200-500ms (Gitea API round-trip), then Cloudflare caches the response.

Key Files

File Purpose
src/lib/content-api.ts getCollectionMeta() — synchronous metadata from build-time index
src/lib/content-backend.ts readContentFile() — Gitea API fetch with local filesystem fallback
scripts/build-content-index.mjs Generates src/data/content-index.json at build time
src/content/config.ts Journal and posts are explicitly NOT registered here

Why Not astro:content?

Registering a collection in src/content/config.ts means Astro pre-processes all files and bundles them into _astro_data-layer-content_*.mjs. If even one SSR route imports getCollection(), the entire data layer ships in the Worker. For 200+ markdown files, this adds 100 KB+ to the Worker — and the pre-existing data layer was already 5.2 MB.

By keeping journal and posts out of config.ts, the Worker only grows by ~20 KB (the route files themselves).

Scaling Math

Metric 213 entries (current) 3,000 entries (projected)
content-index.json 127 KB ~1.8 MB
Route files ~20 KB ~20 KB (unchanged)
Worker bundle impact ~20 KB ~20 KB (unchanged)
Build time impact Negligible Negligible (SSR, not pre-rendered)
First page load ~200-500ms ~200-500ms (same, per-entry fetch)

Content Collections

Collection Count Location Registered in config.ts?
posts 104 src/content/posts/ No
journal 97 src/content/journal/ No
docs ~100+ src/content/docs/ Yes (small, stable)
configurations ~10 src/content/configurations/ Yes (small, stable)
projects ~15 src/content/projects/ Yes (small, stable)

Rule: Only register small, stable collections. Large or growing collections use the SSR + API pattern.

Blog Features

  • Tag filtering — client-side JavaScript filters post cards by tag
  • Tendril knowledge graph — interactive graph visualization with obsidian-tags layout
  • Square hero imagesaspect-ratio: 1/1 with object-fit: cover on both listing cards and post pages
  • Responsive grid — 2-column on desktop, single column on mobile

Journal Features

  • Terminal-style UI — pstree visualization, colored dots, monospace fonts
  • Chronological browsing — entries grouped by year/month in timeline view
  • Prev/next navigation — browse between entries without returning to the listing
  • Back button — quick return to journal index
  • Admin review badges — visible with ?admin=true query parameter
  • Mood indicators — FOCUSED, DEBUG, BUILD, TRIUMPHANT, etc.

Adding New Content

Create a markdown file in the appropriate directory:

src/content/journal/YYYY-MM-DD-slug.md
src/content/posts/YYYY-MM-DD-slug.md

The build script automatically picks up new files. No config changes needed. The content index regenerates on every build.

Date: 2026-03-19

architectureblogjournalssrcontentcloudflare