building-static-sites
Use when creating static websites with Next.js static export - covers YAML-based content, image optimization, form handling, SEO, and deployment to GitHub Pages/Netlify/Vercel
When & Why to Use This Skill
This Claude skill streamlines the development of high-performance static websites using Next.js static export. It provides a comprehensive framework for managing structured content via YAML, optimizing images for fast loading, implementing SEO best practices, and automating deployment to platforms like GitHub Pages, Netlify, and Vercel, effectively solving the complexity of static site architecture and asset management.
Use Cases
- Developing SEO-optimized marketing landing pages and portfolios with fast-loading static assets and automated deployment.
- Building documentation sites or blogs using a type-safe, YAML-based content management system for structured data handling.
- Implementing serverless form handling and optimized media workflows for sites hosted on GitHub Pages or Netlify.
- Setting up robust CI/CD pipelines and build verification scripts to ensure static site integrity before production release.
| name | building-static-sites |
|---|---|
| description | Use when creating static websites with Next.js static export - covers YAML-based content, image optimization, form handling, SEO, and deployment to GitHub Pages/Netlify/Vercel |
Building Static Sites
Overview
Static site generation with Next.js (output: 'export') for marketing sites, documentation, portfolios, and blogs.
Extends building-with-nextjs for static export.
When to Use
- Marketing sites, docs, blogs, portfolios, landing pages
- No auth, no server-side logic, no real-time data
Configuration
next.config.ts:
const nextConfig = {
output: 'export', // Enable static HTML export
images: {
unoptimized: true, // Or use optimizer plugin
},
}
Build output: out/ directory with static HTML
YAML-Based Content (Recommended)
Structure:
content/
├── global.yaml # Site-wide content
├── features.yaml # Reusable blocks
└── pages/
├── home.yaml
├── about.yaml
└── contact.yaml
Type-safe loading:
// lib/content-types.ts
export interface GlobalContent {
site: { title: string; description: string; url: string }
navigation: { links: Array<{ href: string; label: string }> }
}
// lib/content.ts
import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
export function getGlobalContent(): GlobalContent {
const filePath = path.join(process.cwd(), 'content', 'global.yaml')
const content = fs.readFileSync(filePath, 'utf8')
return yaml.load(content) as GlobalContent
}
Example YAML:
# content/global.yaml
site:
title: "My Company"
description: "We build great things"
navigation:
links:
- href: "/"
label: "Home"
- href: "/about"
label: "About"
Usage:
// app/page.tsx
import { getHomeContent } from '@/lib/content'
export default function HomePage() {
const content = getHomeContent()
return <h1>{content.hero.headline}</h1>
}
Dependencies:
pnpm add js-yaml
pnpm add -D @types/js-yaml
Image Optimization
Option 1: Pre-optimize manually
pnpm add -D sharp-cli
sharp resize 1920 --quality 85 --format webp public/images/*.{jpg,png}
Option 2: Build-time optimization
pnpm add -D next-image-export-optimizer
Option 3: CDN-based (Recommended)
- Use Cloudflare Images, imgix, or similar
- No build processing needed
Form Handling
See integrating-formspree-forms skill for complete setup.
Quick example:
'use client'
import { useForm } from '@formspree/react'
export default function ContactForm() {
const [state, handleSubmit] = useForm("YOUR_FORM_ID")
if (state.succeeded) {
return <p>Thanks for your message!</p>
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
)
}
SEO
Metadata configuration:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { default: 'My Site', template: '%s | My Site' },
description: 'Site description',
openGraph: {
title: 'My Site',
description: 'Site description',
images: [{ url: 'https://example.com/og-image.png' }],
},
}
Sitemap generation:
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
priority: 1,
},
]
}
robots.txt:
// app/robots.ts
export default function robots() {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: 'https://example.com/sitemap.xml',
}
}
Testing
Content validation:
// scripts/validate-yaml.js
import fs from 'fs'
import yaml from 'js-yaml'
function validateYaml(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8')
yaml.load(content)
console.log(`✅ ${filePath}`)
} catch (error) {
console.error(`❌ ${filePath}: ${error.message}`)
process.exit(1)
}
}
Build verification:
// scripts/verify-build.js
import fs from 'fs'
import path from 'path'
const requiredPages = ['index.html', 'about/index.html']
requiredPages.forEach(page => {
const fullPath = path.join(process.cwd(), 'out', page)
if (!fs.existsSync(fullPath)) {
console.error(`❌ Missing: ${page}`)
process.exit(1)
}
})
console.log('✅ Build verification passed')
Deployment
GitHub Pages:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: pnpm build
- uses: actions/upload-pages-artifact@v3
with:
path: ./out
Netlify:
# netlify.toml
[build]
command = "pnpm build"
publish = "out"
Vercel: Auto-detects Next.js, no config needed.
justfile Commands
build-static:
pnpm build
@echo "✅ Static site built to ./out"
validate-content:
node scripts/validate-yaml.js
verify-build:
node scripts/verify-build.js
check-all: format lint typecheck validate-content test coverage test-e2e a11y verify-build
@echo "✅ All static site checks passed"
Limitations & Workarounds
No dynamic content:
- ✅ Use client-side fetching for dynamic data
- ✅ Rebuild site periodically (scheduled CI)
No API routes:
- ✅ Use third-party services (Formspree, Supabase)
Image optimization disabled:
- ✅ Use CDN-based optimization
- ✅ Pre-optimize with sharp-cli