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
| Metric | Target |
|---|---|
| Lighthouse Performance | 90+ |
| Lighthouse SEO | 100 |
| Lighthouse Accessibility | 95+ |
| First Contentful Paint | < 1.5s (edge-cached) |
| Core Web Vitals | All 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