惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
美团技术团队
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
S
SegmentFault 最新的问题
博客园_首页
雷峰网
雷峰网
V
V2EX
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)
量子位
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
Jina AI
Jina AI
月光博客
月光博客
L
LangChain Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Static Sites With YAML Data in Next.js 15 App Router
ALi · 2026-05-11 · via DEV Community

ALi

I recently shipped a directory site and went back to a pattern I keep reaching for: YAML files as the data layer for static Next.js sites. No CMS, no database, no API routes. Just files in a directory, read at build time.

This post walks through the setup. It's not flashy — it's the kind of architecture that gets out of your way so you can focus on content.

When this pattern fits

  • You have a known set of entities (products, games, restaurants, neighborhoods, libraries)
  • Content updates daily, not by-the-second
  • You want zero runtime cost
  • You want git to be your version control AND your CMS

If any of those is false, use a database. If all are true, this is the simplest thing that works.

The directory

data/
  games/
    klondike.yaml
    freecell.yaml
    spider.yaml
app/
  games/
    [slug]/
      page.tsx
lib/
  games.ts

Enter fullscreen mode Exit fullscreen mode

Each .yaml file is one page. Filename becomes URL slug. The folder is the database.

One YAML file per entity

# data/games/klondike.yaml
name: "Klondike"
slug: "klondike"
metaTitle: "Klondike Solitaire  Rules, Strategy & Where to Play"
metaDescription: "Learn Klondike solitaire rules, strategy tips, and where to play online. The classic single-deck patience game."
difficulty: 2
deckCount: 1
tags: ["classic", "single-deck", "easy"]
description: >
  Klondike is the most popular solitaire variant…
rules: >
  Deal cards face down across seven tableau columns…

Enter fullscreen mode Exit fullscreen mode

YAML over JSON because long-form prose with newlines is readable. Block scalars (>) collapse whitespace into single paragraphs, which is exactly what you want for content fields.

The loader

// lib/games.ts
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import type { Game } from "./types";

const GAMES_DIR = path.join(process.cwd(), "data", "games");

export function loadGame(slug: string): Game {
  const filePath = path.join(GAMES_DIR, `${slug}.yaml`);
  const content = fs.readFileSync(filePath, "utf-8");
  return yaml.load(content) as Game;
}

export function getAllGameSlugs(): string[] {
  return fs
    .readdirSync(GAMES_DIR)
    .filter((f) => f.endsWith(".yaml"))
    .map((f) => f.replace(".yaml", ""));
}

Enter fullscreen mode Exit fullscreen mode

js-yaml is the standard library. No streaming, no async — readFileSync is fine because this only runs at build time.

The Game type is hand-maintained in lib/types.ts. You could generate it from a JSON Schema if you want runtime validation, but for a small project, a TypeScript interface plus careful YAML editing is enough.

The page

// app/games/[slug]/page.tsx
import { loadGame, getAllGameSlugs } from "@/lib/games";
import { notFound } from "next/navigation";

export function generateStaticParams() {
  return getAllGameSlugs().map((slug) => ({ slug }));
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const game = loadGame(slug);
  return {
    title: game.metaTitle,
    description: game.metaDescription,
  };
}

export default async function GamePage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const game = loadGame(slug);
  if (!game) notFound();

  return (
    <article>
      <h1>{game.name}</h1>
      <section dangerouslySetInnerHTML={{ __html: game.description }} />
      <section>
        <h2>Rules</h2>
        <p>{game.rules}</p>
      </section>
    </article>
  );
}

Enter fullscreen mode Exit fullscreen mode

A few Next.js 15 specifics worth flagging:

  • params is a Promise in Next 15. This tripped me up coming from Next 14. You have to await it before destructuring.
  • generateStaticParams runs at build time. This is what triggers SSG — each returned slug becomes a static HTML file.
  • generateMetadata is called per page. Each entity gets a unique title/description without any extra wiring.

The build output

$ pnpm build
…
Route (app)                                Size  First Load JS
├ ● /games/[slug]                         173 B    113 kB
├   ├ /games/klondike
├   ├ /games/freecell
├   ├ /games/spider
├   └ [+24 more paths]
…

Enter fullscreen mode Exit fullscreen mode

Every YAML file becomes a pre-rendered HTML page. No runtime, no cold start, no database query. Vercel serves them from the edge CDN as flat files.

Sitemap and robots

next-sitemap reads your built routes and generates sitemap.xml automatically. Two files of config:

// next-sitemap.config.js
module.exports = {
  siteUrl: "https://www.solitaireassociation.com",
  generateRobotsTxt: true,
  changefreq: "weekly",
};

Enter fullscreen mode Exit fullscreen mode

// package.json
{
  "scripts": {
    "postbuild": "next-sitemap"
  }
}

Enter fullscreen mode Exit fullscreen mode

Done. Every game page is in the sitemap, with a <lastmod> you can wire to a dateModified field in your YAML.

What I'd watch out for

  • Don't put HTML in YAML. Markdown is fine if you render it with next-mdx-remote or a similar pipeline. Raw HTML in a content field becomes a security/XSS surface you don't want.
  • Keep YAML files small. If your entity description is 5,000 words, split it into separate fields or use MDX. YAML parsers slow down on large block scalars.
  • TypeScript types must match reality. YAML is loose — a typo in a field name silently becomes undefined. Add a runtime check at boot or use a schema library like Zod if this matters to you.
  • Hot reload doesn't always pick up YAML changes. In dev, you sometimes need to restart next dev after editing a YAML file. Not a dealbreaker, but worth knowing.

When to graduate to a CMS

If you start needing:

  • Multiple non-technical editors
  • Workflow (drafts, approvals, scheduled publishing)
  • Image uploads from a UI
  • Localization at scale

…then a headless CMS earns its keep. Until then, YAML + git + Next.js is faster, cheaper, and easier to reason about.

What I built with it

The pattern is currently powering solitaireassociation.com, a directory of solitaire variants. Each game (Klondike, FreeCell, Spider, etc.) is one YAML file. The full build runs in under 30 seconds on Vercel.

If you're building something with a known set of entities and want each to get its own pre-rendered page, this is probably the simplest stack that works.