Skip to content

Commit c3bf834

Browse files
authored
feat: design use cases and model seo pages (#985)
1 parent 9b668f6 commit c3bf834

123 files changed

Lines changed: 7818 additions & 771 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/site-ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,11 @@ jobs:
269269
uses: actions/download-artifact@v4
270270
with:
271271
name: site-build
272-
path: site
272+
path: site/dist
273+
274+
# Blocking: sitemap membership must equal the rendered-indexable set.
275+
- name: Verify sitemap indexability contract
276+
run: pnpm verify:sitemap-indexability
273277

274278
- name: Validate sitemap
275279
id: sitemap

site/astro.config.mjs

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,53 +8,83 @@ import path from 'node:path';
88
import os from 'node:os';
99

1010
import vue from '@astrojs/vue';
11+
import { deriveModelGroups } from './src/lib/workflow-pages/model-groups.ts';
12+
import { SEO_PAGES } from './src/lib/workflow-pages/use-cases.ts';
13+
import { resolveUseCasePageTemplates } from './src/lib/workflow-pages/use-case-resolver.ts';
14+
import {
15+
modelContentPasses,
16+
useCaseContentPasses,
17+
} from './src/lib/workflow-pages/landing-content.ts';
1118

12-
// Build template date lookup at config time
1319
const templatesDir = path.join(process.cwd(), 'src/content/templates');
1420
const templateDates = new Map();
21+
/**
22+
* @typedef {{ name: string; date?: string; username?: string; models?: string[];
23+
* tags?: string[]; usage?: number }} SitemapTemplate
24+
*/
25+
/** Raw content templates, reused below to derive indexable slugs. @type {SitemapTemplate[]} */
26+
const contentTemplates = [];
27+
const creatorUsernames = new Set();
1528

1629
if (fs.existsSync(templatesDir)) {
1730
const files = fs.readdirSync(templatesDir).filter((f) => f.endsWith('.json'));
1831
for (const file of files) {
1932
try {
2033
const content = JSON.parse(fs.readFileSync(path.join(templatesDir, file), 'utf-8'));
21-
if (content.name && content.date) {
22-
templateDates.set(content.name, content.date);
23-
}
34+
if (content.username) creatorUsernames.add(content.username);
35+
if (typeof content.name !== 'string') continue;
36+
// Normalize the arrays the group/use-case resolvers read, so a template
37+
// JSON missing `models`/`tags` can't crash sitemap derivation.
38+
content.models = Array.isArray(content.models) ? content.models : [];
39+
content.tags = Array.isArray(content.tags) ? content.tags : [];
40+
contentTemplates.push(content);
41+
if (content.date) templateDates.set(content.name, content.date);
2442
} catch {
25-
// Skip invalid JSON files
43+
// Skip invalid JSON
2644
}
2745
}
2846
}
2947

30-
// Build timestamp used as lastmod fallback for pages without a specific date
48+
const modelGroups = deriveModelGroups(contentTemplates);
49+
const canonicalModelSlugs = new Set(modelGroups.map((group) => group.slug));
50+
51+
// Same content gate the routes' noindex uses (landing-content.ts), so the
52+
// sitemap can't advertise a page the route renders noindex.
53+
const indexableModelSlugs = new Set(
54+
modelGroups
55+
.filter((group) => group.qualifies && modelContentPasses(group.slug))
56+
.map((group) => group.slug)
57+
);
58+
59+
const indexableUseCaseSlugs = new Set(
60+
SEO_PAGES.filter(
61+
(def) =>
62+
resolveUseCasePageTemplates(def, contentTemplates).length > 0 &&
63+
useCaseContentPasses(def.slug)
64+
).map((def) => def.slug)
65+
);
66+
67+
// Variant → canonical 301s (real, via the Vercel adapter). Skip a variant that is
68+
// itself a canonical slug, so a redirect can never shadow a real page.
69+
const modelSlugRedirects = Object.fromEntries(
70+
modelGroups.flatMap((group) =>
71+
group.redirectFrom
72+
.filter((variant) => !canonicalModelSlugs.has(variant))
73+
.map((variant) => [`/workflows/model/${variant}`, `/workflows/model/${group.slug}/`])
74+
)
75+
);
76+
77+
// lastmod fallback for pages without a specific date.
3178
const buildDate = new Date().toISOString();
3279

3380
// Supported locales (matches src/i18n/config.ts)
3481
const locales = ['en', 'zh', 'zh-TW', 'ja', 'ko', 'es', 'fr', 'ru', 'tr', 'ar', 'pt-BR'];
3582
const nonDefaultLocales = locales.filter((l) => l !== 'en');
3683

37-
// Custom sitemap pages for ISR routes not discovered at build time
3884
const siteOrigin = (process.env.PUBLIC_SITE_ORIGIN || 'https://comfy.org').replace(/\/$/, '');
3985

40-
// Creator profile pages — extract unique usernames from synced templates
41-
const creatorUsernames = new Set();
42-
if (fs.existsSync(templatesDir)) {
43-
const files = fs.readdirSync(templatesDir).filter((f) => f.endsWith('.json'));
44-
for (const file of files) {
45-
try {
46-
const content = JSON.parse(fs.readFileSync(path.join(templatesDir, file), 'utf-8'));
47-
if (content.username) creatorUsernames.add(content.username);
48-
} catch {
49-
// Skip invalid JSON
50-
}
51-
}
52-
}
53-
5486
const creatorPages = [...creatorUsernames].map((u) => `${siteOrigin}/workflows/${u}/`);
55-
const localeCustomPages = nonDefaultLocales.map((locale) =>
56-
`${siteOrigin}/${locale}/workflows/`
57-
);
87+
const localeCustomPages = nonDefaultLocales.map((locale) => `${siteOrigin}/${locale}/workflows/`);
5888
const customPages = [...creatorPages, ...localeCustomPages];
5989

6090
// https://astro.build/config
@@ -71,6 +101,7 @@ export default defineConfig({
71101
prefixDefaultLocale: false, // English at root, others prefixed (/zh/, /ja/, etc.)
72102
},
73103
},
104+
redirects: modelSlugRedirects,
74105
integrations: [
75106
sitemap({
76107
// Use custom filename to avoid collision with Framer's /sitemap.xml
@@ -97,7 +128,6 @@ export default defineConfig({
97128
return item;
98129
}
99130

100-
// Homepage
101131
if (pathname === '/' || pathname === '') {
102132
item.lastmod = buildDate;
103133
// @ts-expect-error - sitemap types are stricter than actual API
@@ -138,30 +168,55 @@ export default defineConfig({
138168
item.priority = 0.6;
139169
return item;
140170
}
171+
// Use-case pages: /workflows/use-cases/{slug}/ or /{locale}/workflows/use-cases/{slug}/
172+
if (pathname.match(/^(?:\/[a-z]{2}(?:-[A-Z]{2})?)?\/workflows\/use-cases\//)) {
173+
// @ts-expect-error - sitemap types are stricter than actual API
174+
item.changefreq = 'weekly';
175+
item.priority = 0.8;
176+
return item;
177+
}
141178

142-
// Default for other pages
143179
// @ts-expect-error - sitemap types are stricter than actual API
144180
item.changefreq = 'weekly';
145181
item.priority = 0.5;
146182
return item;
147183
},
148-
// Exclude OG image routes and legacy redirect pages from sitemap.
149-
// Legacy redirects are /workflows/{slug}/ without a 12-char hex share_id suffix.
150-
// Canonical detail pages are /workflows/{slug}-{shareId}/ (shareId = 12 hex chars).
184+
// Exclude OG image routes and legacy redirect pages. Legacy redirects are
185+
// /workflows/{slug}/ without a 12-char hex share_id suffix; canonical detail
186+
// pages are /workflows/{slug}-{shareId}/ (shareId = 12 hex chars).
151187
filter: (page) => {
152188
if (page.includes('/workflows/og/') || page.includes('/workflows/og.png')) return false;
153-
// Check if this is a workflow detail path (not category/tag/model/creators)
189+
// Only list indexable model pages. Non-qualifying families and
190+
// variant-redirect slugs still resolve to a route but render noindex (or
191+
// 301), so they must stay out of the sitemap.
192+
const modelMatch = page.match(/\/workflows\/model\/([^/]+)\/$/);
193+
if (modelMatch) {
194+
return indexableModelSlugs.has(modelMatch[1]);
195+
}
196+
197+
// Same rule for use-case pages: only list slugs that actually get an
198+
// indexable page, so a noindex/thin use-case never enters the sitemap.
199+
const useCaseMatch = page.match(/\/workflows\/use-cases\/([^/]+)\/$/);
200+
if (useCaseMatch) {
201+
return indexableUseCaseSlugs.has(useCaseMatch[1]);
202+
}
203+
154204
const match = page.match(/\/workflows\/([^/]+)\/$/);
155205
if (match) {
156206
const segment = match[1];
157-
// Skip known sub-paths
158-
if (['category', 'tag', 'model', 'creators'].some((p) => page.includes(`/workflows/${p}/`))) return true;
159-
// Include if it has a share_id suffix (12 hex chars after last hyphen)
207+
if (
208+
['category', 'tag', 'model', 'creators', 'use-cases'].some((p) =>
209+
page.includes(`/workflows/${p}/`)
210+
)
211+
)
212+
return true;
213+
// Include only when the slug carries a share_id suffix (12 hex chars
214+
// after the last hyphen); anything else is a legacy redirect.
160215
const lastHyphen = segment.lastIndexOf('-');
161-
if (lastHyphen === -1) return false; // No hyphen = legacy redirect
216+
if (lastHyphen === -1) return false;
162217
const candidate = segment.slice(lastHyphen + 1);
163218
if (candidate.length === 12 && /^[0-9a-f]+$/.test(candidate)) return true;
164-
return false; // Has hyphen but not a valid share_id = legacy redirect
219+
return false;
165220
}
166221
return true;
167222
},
@@ -174,50 +229,37 @@ export default defineConfig({
174229
skewProtection: true,
175230
}),
176231

177-
// Build performance optimizations
178232
build: {
179-
// Increase concurrency for faster builds on multi-core systems
180233
concurrency: Math.max(1, os.cpus().length),
181-
// Inline small stylesheets automatically
182234
inlineStylesheets: 'auto',
183235
},
184236

185-
// HTML compression
186237
compressHTML: true,
187238

188-
// Image optimization settings
189239
image: {
190240
service: {
191241
entrypoint: 'astro/assets/services/sharp',
192242
config: {
193-
// Limit input pixels to prevent memory issues with large images
194-
limitInputPixels: 268402689, // ~16384x16384
243+
limitInputPixels: 268402689, // ~16384x16384, guards against memory blowups
195244
},
196245
},
197246
},
198247

199-
// Responsive images for automatic srcset generation (now stable in Astro 5)
200-
// Note: responsiveImages was moved from experimental to stable in Astro 5.x
201-
202248
vite: {
203249
plugins: [tailwindcss()],
204250
build: {
205-
// Increase chunk size warning limit (reduces noise)
206251
chunkSizeWarningLimit: 1000,
207252
rollupOptions: {
208253
output: {
209-
// Manual chunking for better caching
210254
manualChunks: {
211255
vendor: ['web-vitals'],
212256
},
213257
},
214258
},
215259
},
216-
// Optimize dependency pre-bundling
217260
optimizeDeps: {
218261
include: ['web-vitals'],
219262
},
220-
// Disable dev sourcemaps for CSS (faster)
221263
css: {
222264
devSourcemap: false,
223265
},

site/creators.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,14 @@
6262
"displayName": "Sirolim",
6363
"handle": "sirolim",
6464
"summary": "Sirolim is a Creative Technologist building templates for ComfyUI. When he's not working on templates or movies, he loves agentic engineering.",
65-
"social": [
66-
"https://x.com/siro_lim",
67-
"https://www.linkedin.com/in/peterschwarz0001/"
68-
],
65+
"social": ["https://x.com/siro_lim", "https://www.linkedin.com/in/peterschwarz0001/"],
6966
"avatarUrl": "/workflows/avatars/sirolim.png"
7067
},
7168
"shane": {
7269
"displayName": "shanef3d",
7370
"handle": "shane",
7471
"summary": "Digital artist and creative director using 3D + AI.",
75-
"social": [
76-
"https://www.instagram.com/shanef3d",
77-
"https://shanef3d.com"
78-
],
72+
"social": ["https://www.instagram.com/shanef3d", "https://shanef3d.com"],
7973
"avatarUrl": "/workflows/avatars/shanef3d.png"
8074
},
8175
"enigmatic_e": {

site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"research:competitors": "tsx scripts/competitor-keywords.ts",
3939
"audit:seo": "tsx scripts/seo-audit.ts",
4040
"validate:sitemap": "tsx scripts/validate-sitemap.ts",
41+
"verify:sitemap-indexability": "tsx scripts/verify-sitemap-indexability.ts",
4142
"research:paa": "tsx scripts/paa-research.ts",
4243
"test:visual": "playwright test visual.spec.ts",
4344
"test:visual:update": "playwright test visual.spec.ts --update-snapshots",

site/public/icons/logo.svg

Lines changed: 3 additions & 0 deletions
Loading

site/public/icons/node-link.svg

Lines changed: 3 additions & 0 deletions
Loading

site/public/icons/plus.svg

Lines changed: 4 additions & 0 deletions
Loading

site/scripts/lib/sync.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ function isAppTemplate(name: string): boolean {
3636
return name.endsWith('.app');
3737
}
3838

39-
function createSyncedTemplate(
40-
template: TemplateInfo,
41-
locale: string,
42-
): SyncedTemplate {
39+
function createSyncedTemplate(template: TemplateInfo, locale: string): SyncedTemplate {
4340
const thumbnails = findThumbnails(template.name);
4441
const workflow = readWorkflowJson(template.name);
4542

@@ -99,7 +96,7 @@ export function runSync(): void {
9996
const appTemplates = allEnTemplates.filter((t) => isAppTemplate(t.name));
10097
if (appTemplates.length > 0) {
10198
logger.info(
102-
`Detected ${appTemplates.length} Comfy App template(s): ${appTemplates.map((t) => t.name).join(', ')}`,
99+
`Detected ${appTemplates.length} Comfy App template(s): ${appTemplates.map((t) => t.name).join(', ')}`
103100
);
104101
}
105102

0 commit comments

Comments
 (0)