@@ -8,53 +8,83 @@ import path from 'node:path';
88import os from 'node:os' ;
99
1010import 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
1319const templatesDir = path . join ( process . cwd ( ) , 'src/content/templates' ) ;
1420const 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
1629if ( 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.
3178const buildDate = new Date ( ) . toISOString ( ) ;
3279
3380// Supported locales (matches src/i18n/config.ts)
3481const locales = [ 'en' , 'zh' , 'zh-TW' , 'ja' , 'ko' , 'es' , 'fr' , 'ru' , 'tr' , 'ar' , 'pt-BR' ] ;
3582const nonDefaultLocales = locales . filter ( ( l ) => l !== 'en' ) ;
3683
37- // Custom sitemap pages for ISR routes not discovered at build time
3884const 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-
5486const 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/` ) ;
5888const 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 } ) ? ) ? \/ w o r k f l o w s \/ u s e - c a s e s \/ / ) ) {
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 ( / \/ w o r k f l o w s \/ m o d e l \/ ( [ ^ / ] + ) \/ $ / ) ;
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 ( / \/ w o r k f l o w s \/ u s e - c a s e s \/ ( [ ^ / ] + ) \/ $ / ) ;
200+ if ( useCaseMatch ) {
201+ return indexableUseCaseSlugs . has ( useCaseMatch [ 1 ] ) ;
202+ }
203+
154204 const match = page . match ( / \/ w o r k f l o w s \/ ( [ ^ / ] + ) \/ $ / ) ;
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 - 9 a - 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 } ,
0 commit comments