{
  "slug": "build-guide",
  "title": "Guide to build.ts — Static Site Generation",
  "description": "Step-by-step guide to building the perfect build.ts for a static HTML site powered by @webhouse/cms.",
  "category": "guides",
  "order": 10,
  "locale": "en",
  "translationGroup": "607d840a-e643-467b-8bd4-b7f5535bde93",
  "helpCardId": null,
  "content": "## What is build.ts?\n\n`build.ts` is a custom static site generator that reads your CMS content (JSON files) and outputs plain HTML. No framework, no runtime JavaScript — just HTML + CSS that works everywhere.\n\n```bash\nnpx tsx build.ts    # Generate dist/\n```\n\n## Lesson 1: Load content\n\nThe foundation — read JSON files from `content/`:\n\n```typescript\nimport { readFileSync, readdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst CONTENT = join(import.meta.dirname, 'content');\n\nfunction getCollection(name: string) {\n  const dir = join(CONTENT, name);\n  if (!existsSync(dir)) return [];\n  return readdirSync(dir)\n    .filter(f => f.endsWith('.json'))\n    .map(f => JSON.parse(readFileSync(join(dir, f), 'utf-8')))\n    .filter(d => d.status === 'published');\n}\n\nfunction getDocument(collection: string, slug: string) {\n  const file = join(CONTENT, collection, slug + '.json');\n  if (!existsSync(file)) return null;\n  return JSON.parse(readFileSync(file, 'utf-8'));\n}\n```\n\n## Lesson 2: Render markdown\n\nConvert richtext content to HTML:\n\n```typescript\nimport { marked } from 'marked';\n\nfunction renderMarkdown(content: string): string {\n  return marked.parse(content, { async: false }) as string;\n}\n```\n\n## Lesson 3: HTML template\n\nWrap content in a complete HTML document:\n\n```typescript\nfunction htmlPage(title: string, body: string, css: string): string {\n  return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>${title}</title>\n  <style>${css}</style>\n</head>\n<body>\n  ${body}\n</body>\n</html>`;\n}\n```\n\n## Lesson 4: Build a page\n\nCombine content loading, markdown rendering, and HTML template:\n\n```typescript\nfunction buildPage(doc: any, css: string): string {\n  const title = doc.data.title;\n  const content = renderMarkdown(doc.data.content || '');\n  const body = `\n    <article>\n      <h1>${title}</h1>\n      ${content}\n    </article>\n  `;\n  return htmlPage(title, body, css);\n}\n```\n\n## Lesson 5: Render blocks\n\nIf your pages use blocks (hero, features, CTA), render each block type:\n\n```typescript\ninterface Block { _block: string; [key: string]: unknown; }\n\nfunction renderBlock(block: Block): string {\n  switch (block._block) {\n    case 'hero':\n      return `<section class=\"hero\">\n        <h1>${block.tagline}</h1>\n        <p>${block.description}</p>\n      </section>`;\n    case 'features':\n      const items = (block.items as any[]) || [];\n      return `<section class=\"features\">\n        <h2>${block.title}</h2>\n        <div class=\"grid\">\n          ${items.map(i => `<div><h3>${i.title}</h3><p>${i.description}</p></div>`).join('')}\n        </div>\n      </section>`;\n    case 'cta':\n      return `<section class=\"cta\">\n        <h2>${block.title}</h2>\n        <a href=\"${block.buttonUrl}\">${block.buttonText}</a>\n      </section>`;\n    default:\n      return '';\n  }\n}\n\nfunction renderBlocks(blocks: Block[]): string {\n  return blocks.map(renderBlock).join('\\n');\n}\n```\n\n## Lesson 6: SEO metadata\n\nExtract SEO fields and generate meta tags:\n\n```typescript\nfunction seoTags(doc: any): string {\n  const seo = doc.data._seo || {};\n  const title = seo.metaTitle || doc.data.title;\n  const desc = seo.metaDescription || doc.data.excerpt || '';\n  return `\n    <title>${title}</title>\n    <meta name=\"description\" content=\"${desc}\">\n    <meta property=\"og:title\" content=\"${title}\">\n    <meta property=\"og:description\" content=\"${desc}\">\n    ${seo.ogImage ? `<meta property=\"og:image\" content=\"${seo.ogImage}\">` : ''}\n  `;\n}\n```\n\n## Lesson 7: Write output\n\nGenerate files to `dist/`:\n\n```typescript\nimport { writeFileSync, mkdirSync } from 'node:fs';\n\nconst DIST = join(import.meta.dirname, 'dist');\n\nfunction writePage(urlPath: string, html: string) {\n  const dir = join(DIST, urlPath);\n  mkdirSync(dir, { recursive: true });\n  writeFileSync(join(dir, 'index.html'), html);\n}\n\n// Build all pages\nconst posts = getCollection('posts');\nfor (const post of posts) {\n  const html = buildPage(post, css);\n  writePage(`/blog/${post.slug}`, html);\n}\n```\n\n## Lesson 8: Sitemap\n\nGenerate `sitemap.xml` for search engines:\n\n```typescript\nfunction generateSitemap(baseUrl: string, pages: string[]): string {\n  const urls = pages.map(p =>\n    `  <url><loc>${baseUrl}${p}</loc></url>`\n  ).join('\\n');\n  return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls}\n</urlset>`;\n}\n```\n\n## Lesson 9: Resolve snippets\n\nAdd snippet support to your build:\n\n```typescript\nfunction resolveSnippets(markdown: string): string {\n  return markdown.replace(\n    /\\{\\{snippet:([a-z0-9-]+)\\}\\}/g,\n    (_match, slug) => {\n      const snippet = getDocument('snippets', slug);\n      if (!snippet) return '';\n      return '\\x60\\x60\\x60' + (snippet.data.lang || 'text') +\n        '\\n' + snippet.data.code + '\\n\\x60\\x60\\x60';\n    }\n  );\n}\n\n// Use in your build pipeline:\nconst content = resolveSnippets(doc.data.content);\nconst html = renderMarkdown(content);\n```\n\n## Lesson 10: Copy static assets\n\nCopy uploads and public files:\n\n```typescript\nimport { cpSync } from 'node:fs';\n\n// Copy uploads\ncpSync(join(import.meta.dirname, 'public', 'uploads'),\n       join(DIST, 'uploads'), { recursive: true });\n\n// Copy favicon\ncpSync(join(import.meta.dirname, 'public', 'favicon.svg'),\n       join(DIST, 'favicon.svg'));\n```\n\n## The complete build pipeline\n\nPutting it all together:\n\n```typescript\n// 1. Load CSS (inline in HTML)\nconst css = readFileSync('styles.css', 'utf-8');\n\n// 2. Build collection index pages\nconst posts = getCollection('posts');\nwritePage('/blog', buildListPage('Blog', posts, css));\n\n// 3. Build individual pages\nfor (const post of posts) {\n  writePage(`/blog/${post.slug}`, buildPage(post, css));\n}\n\n// 4. Build homepage\nconst home = getDocument('pages', 'home');\nif (home) writePage('/', buildPage(home, css));\n\n// 5. Generate sitemap\nconst allPaths = ['/'].concat(posts.map(p => `/blog/${p.slug}`));\nwriteFileSync(join(DIST, 'sitemap.xml'), generateSitemap(BASE_URL, allPaths));\n\n// 6. Copy assets\ncpSync('public/uploads', join(DIST, 'uploads'), { recursive: true });\n\nconsole.log('Built ' + allPaths.length + ' pages');\n```\n\n## Next steps\n\n- [Templates](/docs/templates) — start from a working boilerplate instead of scratch\n- [Next.js Patterns](/docs/nextjs-patterns) — if you want React instead of static HTML\n- [Shared Snippets](/docs/shared-snippets) — reusable code blocks across pages",
  "excerpt": "What is build.ts?\n\nbuild.ts is a custom static site generator that reads your CMS content (JSON files) and outputs plain HTML. No framework, no runtime JavaScript — just HTML + CSS that works everywhere.\n\nbash\nnpx tsx build.ts     Generate dist/\n\n\n Lesson 1: Load content\n\nThe foundation — read JSON ",
  "seo": {
    "metaTitle": "Guide to build.ts — Static Site Generation — webhouse.app Docs",
    "metaDescription": "Step-by-step guide to building the perfect build.ts for a static HTML site powered by @webhouse/cms."
  },
  "createdAt": "2026-03-30T12:14:24.904Z",
  "updatedAt": "2026-03-30T12:14:24.904Z"
}