Zero-JS Content Site: Astro, Keystatic, and Sub-10KB Pages

· updated Jun 11, 2026 · 5 min read

This site ships zero client-side JavaScript on public pages, loads under 10KB per page, and scores Lighthouse 100/100 — here's the exact stack and why each piece earned its place.

What I Built: Three Requirements, One Stack

A personal site with three requirements:

  • Easy publishing: edit in a UI, commit to git, auto-deploy
  • Fast and minimal: no frontend frameworks, pages under 10KB
  • Clean URLs: posts at /post-title instead of /posts/post-title

Stack: Astro, Keystatic, Vercel.

Why Astro: Zero JavaScript by Default, No Runtime, No Bundle Parse

Astro ships zero JavaScript by default. These pages are HTML and CSS—no React runtime, no hydration, no bundle to parse.

For a content site, this is exactly right. I need fast loads and good SEO, not interactivity. Astro's content collections give me type-safe frontmatter and easy querying with no runtime cost.

Why Keystatic: Git-Backed CMS with No Separate Backend and Version History Built In

I needed a CMS that stores content in git, has a decent editing UI, works locally and in production, and doesn't require a separate backend.

Keystatic does all of this. Content lives in .mdoc files in the repo. I can edit locally in VS Code or use the admin UI at /keystatic. Changes go through git, so version history is built in.

Trade-off: the admin UI adds ~850KB to the /keystatic route. But that only loads when I'm editing—public pages don't touch it.

The Full Project Structure

src/
  content/
    posts/       # blog posts (mdoc)
    pages/       # about, now (mdoc)
    settings/    # site config (yaml)
  pages/
    [slug].astro # dynamic post routes
    writing.astro
    about.astro
    now.astro
  styles/
    global.css   # ~1.5KB gzipped

CSS is one file. No CSS-in-JS, no Tailwind, no extra build step. Custom properties for colors and spacing, system fonts. Dark mode via prefers-color-scheme.

The Four Content Types Keystatic Manages

Keystatic manages four content types:

Posts — title, description, published/updated dates, draft toggle, featured image, markdoc content. Draft posts are filtered from all public pages.

Pages — about and now pages, each with title, meta description, and markdoc content. The now page has a lastUpdated date that displays automatically.

Homepage — headline and intro text, editable separately from code.

Settings — site name, description, URL, social links. These populate meta tags across all pages.

How Posts Render at /slug Without a /posts/ Prefix

Posts render at /{slug} instead of /posts/{slug}. I moved the dynamic route from pages/posts/[slug].astro to pages/[slug].astro and added a reserved slugs check so posts can't collide with /about or /now.

const reserved = ['about', 'now', 'writing', 'keystatic', 'rss'];
const posts = (await getCollection('posts')).filter(p => !p.data.draft);
return posts
  .filter(post => !reserved.includes(post.slug))
  .map(post => ({ params: { slug: post.slug }, props: { post } }));

RSS and Sitemap: Auto-Generated on Every Push, No Manual Work

RSS feed at /rss.xml, auto-generated from published posts. Sitemap at /sitemap-index.xml via @astrojs/sitemap. Both update on every build—no manual work.

The RSS feed link is in the HTML head, so feed readers can auto-discover it.

Structured Data: JSON-LD, OpenGraph, and Canonical URLs Without Plugins

Every page gets:

  • JSON-LD structured dataArticle schema for posts, WebSite for pages
  • OpenGraph tags — title, description, URL, type
  • Twitter Card tags — summary card with title and description
  • Canonical URLs — prevents duplicate content issues
  • Article timestampsarticle:published_time and article:modified_time for posts

This makes the site readable by search engines and LLMs. No plugins—just meta tags generated in the layout component.

const jsonLd = {
  '@context': 'https://schema.org',
  '@type': article ? 'Article' : 'WebSite',
  name: pageTitle,
  description,
  url: canonical.toString(),
  ...(article && {
    datePublished: article.published,
    dateModified: article.modified ?? article.published,
    author: { '@type': 'Person', name: siteName },
    publisher: { '@type': 'Organization', name: siteName }
  })
};

Accessibility: Semantic HTML and System Fonts Without Extra Libraries

  • Semantic HTML<article>, <header>, <main>, <nav>, <time> elements
  • Active page indicatoraria-current="page" on the current nav link
  • System fonts — no layout shift from font loading
  • Keyboard navigation — focus-visible outlines on interactive elements
  • Color contrast — tested in both light and dark modes

Performance: 1.5KB CSS, Sub-10KB Pages, Lighthouse 100/100

  • CSS: 1.5KB gzipped
  • HTML per page: 3-5KB
  • Total page weight: under 10KB (excluding images)
  • No JavaScript on public pages
  • Lighthouse: 100/100 across performance, accessibility, best practices, SEO

Deployment: Push to Main, Live in Under 10 Seconds

Push to main, Vercel builds and deploys. The site is statically generated at build time—no server functions for public pages. Keystatic Cloud handles authentication for the admin UI in production.

Build time is under 10 seconds.

What I'd Do Differently

Honestly, not much. The stack is simple and does what I need. If I were building something with more interactivity, I'd look at Astro's island architecture for selective hydration. But for a content site, zero JS is the right call.

Resources

The source is on GitHub if you want to see how it all fits together.

Frequently Asked Questions

Why use Astro for a personal site?
Astro ships zero JavaScript by default, producing HTML and CSS pages under 10KB with no React runtime, no hydration overhead, and no bundle to parse. For a content site, this means fast loads, better SEO, and Lighthouse 100/100 scores without any performance engineering effort.
What is Keystatic and how does it work?
Keystatic is a git-based CMS that stores content in .mdoc files directly in the repository. It provides a local and production editing UI, requires no separate backend, and gives you full version history through git. Changes deploy automatically — edit in the UI, commit to git, Vercel builds and deploys in under 10 seconds.
How do you get clean post URLs without /posts/ prefix in Astro?
Move the dynamic route from pages/posts/[slug].astro to pages/[slug].astro and add a reserved slugs filter to prevent collisions with /about, /now, /writing, and other top-level routes. Posts then render at /{slug} instead of /posts/{slug} with no redirects needed.
Does Keystatic add JavaScript to public pages?
No. The Keystatic admin UI adds approximately 850KB to the /keystatic route, but public pages — every post, the homepage, about, now, writing — load zero JavaScript by default. The admin route is only ever accessed when editing content, never by readers or search crawlers.
What structured data does this site implement?
Every page gets Article or WebSite JSON-LD schema, OpenGraph tags, Twitter Card tags, and canonical URLs. Post pages add FAQ schema for structured Q&A extraction, BreadcrumbList schema for navigation context, and article:published_time and article:modified_time meta tags. No plugins — all generated in the layout component.
Back to writing