building-static-sites

bryonjacob's avatarfrom bryonjacob

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

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 27, 2025⬇️1downloads

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.
namebuilding-static-sites
descriptionUse 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

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