Skip to main content
SEO Score 100
Case Study

SEO Blog Platform

Search & Signal — A production-ready, content-first blog platform built for search visibility, performance, and editorial flexibility

Overview

A modern technical blog platform ("Search & Signal") featuring a Sanity Studio headless CMS for content authoring and an Astro 7.x server-rendered frontend deployed on Vercel. The platform was built from scratch with SEO as the primary constraint — every architectural decision prioritizes search engine discoverability, Core Web Vitals, and fast first-contentful paint.

Architecture


The platform follows a **monorepo** structure with two independent workspaces:

seo-blog-platform/
├── web/                         # Astro frontend (SSG + on-demand ISR)
│   ├── src/
│   │   ├── pages/                 # Routes: index, blog, category, author, API
│   │   ├── components/            # 14+ Astro UI components
│   │   ├── layouts/               # Single root layout
│   │   ├── lib/                   # Sanity client, GROQ queries, SEO utilities
│   │   └── styles/                # Tailwind v4 + custom prose CSS
│   ├── static/                    # Favicons, OG image assets
│   └── scripts/                   # Dependency-free asset generator
├── studio/                      # Sanity Studio (content authoring)
│   ├── schemas/                   # post, author, category + block types
│   └── lib/                       # Seed script (5 articles, 1 author, 5 categories)
├── package.json                   # Root workspace
└── pnpm-workspace.yaml

Data flow: Content is authored in Sanity Studio → synced to Sanity's CDN → Astro fetches via GROQ at build time (for prerendered routes) or request time (for ISR routes) → HTML sent to client with zero runtime JS for content rendering.

Key Design Decisions

  • Astro 7.x server-render mode via @astrojs/vercel for on-demand ISR
  • Sanity Studio v3 as standalone workspace for content authoring
  • Tailwind CSS v4 via @tailwindcss/vite for styling
  • astro-portabletext for rendering Portable Text with custom block types
  • Fuse.js for client-side fuzzy search, lazy-loaded on first interaction
  • pnpm monorepo workspace for unified dependency management

Features Implemented

Editorial Content Model

  • Post schema with required title, slug, excerpt (max 200 chars), publishedAt, and cover image with required alt text
  • SEO overrides — per-post seoTitle (max 60 chars) and seoDescription (max 160 chars) fields editable directly in Studio
  • Reference relationships between posts, authors, and categories
  • Custom block types: code blocks (with language selection + optional filename), YouTube embeds, callouts (info/warning/success), and images with alt/caption fields
  • Auto-calculated read time (read-only field computed from content word count)

Comprehensive SEO Foundation

  • Per-page meta tags — dynamic <title>, meta description, canonical URL, Open Graph, and Twitter Card tags on every route
  • JSON-LD structured data — BlogPosting schema on articles, WebSite schema on all other pages
  • Dynamic sitemap.xml — generated from Sanity, includes posts, categories, and authors with lastmod timestamps
  • Dynamic robots.txt — references the production sitemap URL
  • RSS feed at /rss.xml via @astrojs/rss
  • Heading anchors synchronized with a live table of contents (h2/h3)
  • Image optimization — explicit width/height attributes, loading=eager, fetchpriority=high, and descriptive alt text enforced in schema

On-Demand ISR with Webhook Revalidation

  • A Vercel webhook endpoint (/api/revalidate) receives Sanity publish events
  • Verifies the x-sanity-webhook-secret header before processing
  • Maps changed document types to affected routes (e.g., a post update revalidates the article page, blog index, and homepage)
  • Triggers Vercel's on-demand revalidation API to purge cached pages at the edge

Client-Side Fuzzy Search

  • A search index is built at build time and stored as JSON at /blog/search.json
  • Fuse.js is lazy-loaded only on first search interaction (no impact on initial load)
  • Searches across titles, excerpts, and categories with fuzzy matching

Performance Optimizations

  • Server-rendered HTML with zero client JavaScript for content delivery
  • Prerendered homepage; ISR-cached dynamic pages with 7-day edge cache
  • Preloaded Google Fonts (Inter) with display=swap and preconnect hints
  • Sanity CDN image transforms with format negotiation (AVIF/WebP)
  • Dark/light theme toggle with localStorage persistence and prefers-color-scheme detection
  • Responsive images with explicit dimensions preventing layout shift

Content Rich Components

  • Portable Text renderer with custom Astro components for all block types
  • Code blocks with copy-to-clipboard functionality, language tagging, and optional filenames
  • YouTube embeds rendered as responsive iframes with aspect-ratio containers
  • Social sharing with pre-populated share text

Challenges & Solutions

ISR route purging on content updates

A minimal /api/revalidate endpoint that receives webhook payloads, validates the secret, maps document types to affected URL paths, and calls Vercel's on-demand revalidation API. Path-mapping logic is type-aware — a post update revalidates the article page, the paginated blog index, and the homepage; a category update revalidates the category page and homepage.

Search without backend or external service

Build-time generated JSON index (/blog/search.json) queried client-side using Fuse.js. The library is lazy-loaded on first search interaction, keeping the initial bundle minimal.

SEO title truncation without double truncation

SEO titles stay under 60 characters. Truncation happens at render time in the SEOHead component — not at the CMS level — preserving data integrity while ensuring correct display.

Portable Text heading IDs and TOC sync

A utility (getContentHeadings) generates slug-based IDs with collision handling (e.g., my-heading-2 for duplicates) and synchronizes them between the rendered content and the TableOfContents component, ensuring anchor links work correctly.

Monorepo dev workflow

Root pnpm -r dev command starts both workspaces, with individual scripts (dev:web, dev:studio) for focused work. The seed script (pnpm seed) populates the Sanity dataset with realistic sample content including all custom block types.

Results

MetricTarget
Lighthouse Performance90+
Lighthouse SEO100
Lighthouse Accessibility95+
First Contentful Paint< 1.5s (edge-cached)
Core Web VitalsAll green (LCP, FID, CLS)

Future Enhancements

  • Additional custom block types for richer content
  • Advanced analytics integration
  • Multi-language support
  • Comment system with moderation
  • Newsletter integration

Deployment

Frontend

Vercel (on-demand ISR)

Backend

Database

Sanity CDN

CI/CD

GitHub Actions

Tech Stack

AstroSanityTailwind CSSTypeScriptFuse.jsVercel