A simple, folder-driven static-site engine.
bun ssg fs
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 114 lines 3.4 kB view raw
1/** 2 * Route generation for collections/entries/blocks (slug-based, config-aware). 3 */ 4import type { Block, Collection, Entry, Site } from "./model"; 5 6type CollectionConfig = { 7 path?: string; 8 name?: string; 9 route?: string; 10}; 11 12function normalizePath(value: unknown): string | undefined { 13 return typeof value === "string" && value ? value.replace(/\\/g, "/") : undefined; 14} 15 16function normalizeRouteSegment(value: unknown): string | undefined { 17 if (typeof value !== "string") return undefined; 18 const trimmed = value.trim().replace(/^\/+|\/+$/g, ""); 19 return trimmed || undefined; 20} 21 22function routeSegments(base: string): string[] { 23 return base.split("/").filter(Boolean); 24} 25 26function joinRoute(...segments: string[]): string { 27 return segments.filter(Boolean).join("/"); 28} 29 30function ensureLeadingSlash(value: string): string { 31 return value.startsWith("/") ? value : `/${value}`; 32} 33 34function resolveBlockRoutes(block: Block, parentSegments: string[]): Block { 35 // Blocks inherit the parent route segments for structure, but the route itself 36 // is set later by resolvers/processors when needed. 37 const segments = [...parentSegments, block.slug]; 38 const subBlocks = block.subBlocks?.map((sub) => resolveBlockRoutes(sub, segments)); 39 40 return { 41 ...block, 42 ...(subBlocks ? { subBlocks } : {}), 43 }; 44} 45 46function resolveEntryRoutes(entry: Entry, collectionRoute: string): Entry { 47 // Entries live under the collection route; blocks nest beneath entries. 48 const baseSegments = routeSegments(collectionRoute); 49 const parentSegments = [...baseSegments, entry.slug]; 50 const route = ensureLeadingSlash(`${joinRoute(...parentSegments)}/`); 51 const blocks = entry.blocks.map((block) => 52 resolveBlockRoutes(block, parentSegments) 53 ); 54 55 return { 56 ...entry, 57 route, 58 blocks, 59 }; 60} 61 62function resolveCollectionRoutes( 63 collection: Collection, 64 config?: Record<string, unknown> 65): Collection { 66 const collectionConfig = config as CollectionConfig | undefined; 67 const configuredRoute = normalizeRouteSegment(collectionConfig?.route); 68 69 const baseRoute = configuredRoute ?? collection.slug; 70 const route = ensureLeadingSlash(`${joinRoute(baseRoute)}/`); 71 const entries = collection.entries.map((entry) => 72 resolveEntryRoutes(entry, baseRoute) 73 ); 74 75 return { 76 ...collection, 77 route, 78 entries, 79 }; 80} 81 82function buildConfigIndex(site: Site): Map<string, CollectionConfig> { 83 // Build a lookup keyed by configured path (or collection key) for fast matching. 84 const map = new Map<string, CollectionConfig>(); 85 const raw = site.config?.collections; 86 if (!raw || typeof raw !== "object") return map; 87 88 for (const [key, value] of Object.entries(raw)) { 89 if (!value || typeof value !== "object") continue; 90 const cfg = value as CollectionConfig; 91 const path = normalizePath(cfg.path) ?? key; 92 map.set(path, cfg); 93 } 94 95 return map; 96} 97 98export function resolveRoutes(site: Site): Site { 99 const configIndex = buildConfigIndex(site); 100 101 const collections = site.collections?.map((collection) => { 102 // Match config by normalized path (preferred) or rawName as fallback. 103 const matchConfig = 104 configIndex.get(collection.path) ?? 105 configIndex.get(normalizePath(collection.rawName) ?? ""); 106 return resolveCollectionRoutes(collection, matchConfig); 107 }); 108 109 return { 110 ...site, 111 route: "/", 112 ...(collections ? { collections } : {}), 113 }; 114}