feat(gallery): canonical templates.json catalog API (SoT -> build-time gen)

Emit public/templates.json from app/templates-data.ts (the single source of
truth) via a prebuild step, so gallery.hanzo.ai/templates.json is the
machine-readable template catalog consumed by hanzo.app. Each record keeps
the raw SoT fields and adds absolute screenshotUrl/templateUrl/repo so
consumers never encode the gallery's internal path scheme.

- scripts/gen-templates-json.mjs: imports templates-data.ts (node 24 type-strip)
- package.json: prebuild hook runs the generator before next build
- Dockerfile: node:22 -> node:24 for stable TS type-stripping
- public/templates.json: 72 templates, 71 with real screenshots

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zeekay
2026-07-02 22:09:17 -07:00
co-authored by Claude Opus 4.8
parent 254b139c40
commit 43c689503a
5 changed files with 1985 additions and 1 deletions
+4 -1
View File
@@ -3,7 +3,10 @@
# Two stages: build the export, then re-serve it through the canonical static server.
# 1) Build the Next.js static export -> /app/out
FROM node:22-bookworm-slim AS build
# node:24 (LTS) has stable TypeScript type-stripping so the `prebuild` step can
# `import` app/templates-data.ts directly to emit public/templates.json (the SoT
# -> canonical catalog API). No flags needed.
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
+2
View File
@@ -4,6 +4,8 @@
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"gen-templates-json": "node scripts/gen-templates-json.mjs",
"prebuild": "node scripts/gen-templates-json.mjs",
"build": "next build",
"start": "next start",
"lint": "eslint",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 236 KiB

File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env node
// One source of truth -> canonical catalog API.
// Reads app/templates-data.ts (the SoT) and emits public/templates.json so
// gallery.hanzo.ai/templates.json is the machine-readable template catalog
// consumed by hanzo.app. Run as a prebuild step (npm run build).
//
// Each emitted record keeps the raw SoT fields and adds absolute URLs so
// consumers never have to know the gallery's internal path scheme:
// screenshotUrl -> https://gallery.hanzo.ai/screenshots/<screenshot>.png
// templateUrl -> https://gallery.hanzo.ai/templates/<slug>
// repo -> https://github.com/hanzo-apps/<slug> (fork source)
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { templates } from '../app/templates-data.ts';
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, '..');
const GALLERY_ORIGIN = process.env.GALLERY_ORIGIN || 'https://gallery.hanzo.ai';
const screenshotsDir = join(root, 'public', 'screenshots');
const enriched = templates.map((t) => {
const shotFile = `${t.screenshot}.png`;
const hasShot = existsSync(join(screenshotsDir, shotFile));
return {
...t,
screenshotUrl: `${GALLERY_ORIGIN}/screenshots/${shotFile}`,
hasScreenshot: hasShot,
templateUrl: `${GALLERY_ORIGIN}/templates/${t.slug}`,
repo: `https://github.com/hanzo-apps/${t.slug}`,
};
});
const withShots = enriched.filter((t) => t.hasScreenshot).length;
const payload = {
version: 1,
origin: GALLERY_ORIGIN,
count: enriched.length,
screenshots: withShots,
generatedAt: new Date().toISOString(),
templates: enriched,
};
const out = join(root, 'public', 'templates.json');
writeFileSync(out, JSON.stringify(payload, null, 2));
console.log(
`gen-templates-json: wrote ${enriched.length} templates (${withShots} with screenshots) -> public/templates.json`,
);