@stati/core 1.16.3 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/core/build.d.ts +41 -12
  2. package/dist/core/build.d.ts.map +1 -1
  3. package/dist/core/build.js +95 -13
  4. package/dist/core/index.d.ts +1 -1
  5. package/dist/core/index.d.ts.map +1 -1
  6. package/dist/core/markdown.d.ts +19 -2
  7. package/dist/core/markdown.d.ts.map +1 -1
  8. package/dist/core/markdown.js +81 -2
  9. package/dist/core/templates.d.ts +11 -2
  10. package/dist/core/templates.d.ts.map +1 -1
  11. package/dist/core/templates.js +28 -11
  12. package/dist/core/utils/index.d.ts +1 -0
  13. package/dist/core/utils/index.d.ts.map +1 -1
  14. package/dist/core/utils/index.js +2 -0
  15. package/dist/core/utils/slugify.utils.d.ts +22 -0
  16. package/dist/core/utils/slugify.utils.d.ts.map +1 -0
  17. package/dist/core/utils/slugify.utils.js +108 -0
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/metrics/index.d.ts +38 -0
  22. package/dist/metrics/index.d.ts.map +1 -0
  23. package/dist/metrics/index.js +42 -0
  24. package/dist/metrics/noop.d.ts +12 -0
  25. package/dist/metrics/noop.d.ts.map +1 -0
  26. package/dist/metrics/noop.js +88 -0
  27. package/dist/metrics/recorder.d.ts +31 -0
  28. package/dist/metrics/recorder.d.ts.map +1 -0
  29. package/dist/metrics/recorder.js +176 -0
  30. package/dist/metrics/types.d.ts +236 -0
  31. package/dist/metrics/types.d.ts.map +1 -0
  32. package/dist/metrics/types.js +7 -0
  33. package/dist/metrics/utils/index.d.ts +9 -0
  34. package/dist/metrics/utils/index.d.ts.map +1 -0
  35. package/dist/metrics/utils/index.js +9 -0
  36. package/dist/metrics/utils/system.utils.d.ts +44 -0
  37. package/dist/metrics/utils/system.utils.d.ts.map +1 -0
  38. package/dist/metrics/utils/system.utils.js +95 -0
  39. package/dist/metrics/utils/writer.utils.d.ts +64 -0
  40. package/dist/metrics/utils/writer.utils.d.ts.map +1 -0
  41. package/dist/metrics/utils/writer.utils.js +145 -0
  42. package/dist/types/config.d.ts +2 -0
  43. package/dist/types/config.d.ts.map +1 -1
  44. package/dist/types/content.d.ts +23 -4
  45. package/dist/types/content.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +1 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/package.json +1 -1
@@ -1,7 +1,22 @@
1
1
  import type { BuildStats, Logger } from '../types/index.js';
2
+ import type { BuildMetrics } from '../metrics/index.js';
3
+ /**
4
+ * Options for metrics collection during builds.
5
+ */
6
+ export interface MetricsOptions {
7
+ /** Enable metrics collection */
8
+ enabled?: boolean;
9
+ /** Include per-page timing details */
10
+ detailed?: boolean;
11
+ }
2
12
  /**
3
13
  * Options for customizing the build process.
4
14
  *
15
+ * @remarks
16
+ * The `| undefined` on optional properties is intentional due to
17
+ * `exactOptionalPropertyTypes: true` in tsconfig.json, allowing callers
18
+ * to explicitly pass `undefined` values.
19
+ *
5
20
  * @example
6
21
  * ```typescript
7
22
  * const options: BuildOptions = {
@@ -9,23 +24,33 @@ import type { BuildStats, Logger } from '../types/index.js';
9
24
  * clean: true, // Clean output directory before build
10
25
  * configPath: './custom.config.js', // Custom config file path
11
26
  * includeDrafts: true, // Include draft pages in build
12
- * version: '1.0.0' // Version to display in build messages
27
+ * version: '1.0.0', // Version to display in build messages
28
+ * metrics: { enabled: true, detailed: true } // Enable metrics collection
13
29
  * };
14
30
  * ```
15
31
  */
16
32
  export interface BuildOptions {
17
33
  /** Force rebuild of all pages, ignoring cache */
18
- force?: boolean;
34
+ force?: boolean | undefined;
19
35
  /** Clean the output directory before building */
20
- clean?: boolean;
36
+ clean?: boolean | undefined;
21
37
  /** Path to a custom configuration file */
22
- configPath?: string;
38
+ configPath?: string | undefined;
23
39
  /** Include draft pages in the build */
24
- includeDrafts?: boolean;
40
+ includeDrafts?: boolean | undefined;
25
41
  /** Custom logger for build output */
26
- logger?: Logger;
42
+ logger?: Logger | undefined;
27
43
  /** Version information to display in build messages */
28
- version?: string;
44
+ version?: string | undefined;
45
+ /** Metrics collection options */
46
+ metrics?: MetricsOptions | undefined;
47
+ }
48
+ /**
49
+ * Extended build result including optional metrics.
50
+ */
51
+ export interface BuildResult extends BuildStats {
52
+ /** Build metrics (only present when metrics enabled) */
53
+ buildMetrics?: BuildMetrics;
29
54
  }
30
55
  /**
31
56
  * Builds the Stati site with ISG support.
@@ -43,20 +68,24 @@ export interface BuildOptions {
43
68
  * 5. Update cache manifest
44
69
  *
45
70
  * @param options - Build configuration options
46
- * @returns Promise resolving to build statistics
71
+ * @returns Promise resolving to build result with statistics and optional metrics
47
72
  * @throws {Error} When configuration is invalid
48
73
  * @throws {Error} When template rendering fails
49
74
  * @throws {Error} When build lock cannot be acquired
50
75
  *
51
76
  * @example
52
77
  * ```typescript
53
- * const stats = await build({
78
+ * const result = await build({
54
79
  * force: true,
55
80
  * clean: true,
56
- * includeDrafts: false
81
+ * includeDrafts: false,
82
+ * metrics: { enabled: true }
57
83
  * });
58
- * console.log(`Built ${stats.pageCount} pages in ${stats.buildTime}ms`);
84
+ * console.log(`Built ${result.totalPages} pages in ${result.buildTimeMs}ms`);
85
+ * if (result.buildMetrics) {
86
+ * console.log(`Cache hit rate: ${result.buildMetrics.isg.cacheHitRate}`);
87
+ * }
59
88
  * ```
60
89
  */
61
- export declare function build(options?: BuildOptions): Promise<BuildStats>;
90
+ export declare function build(options?: BuildOptions): Promise<BuildResult>;
62
91
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAMP,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA4FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAW3E"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAMP,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AA4FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAW5E"}
@@ -1,5 +1,6 @@
1
1
  import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, autoInjectBundles, getBundlePathsForPage, } from './utils/index.js';
2
2
  import { join, dirname, relative, posix } from 'node:path';
3
+ import { performance } from 'node:perf_hooks';
3
4
  import { loadConfig } from '../config/loader.js';
4
5
  import { loadContent } from './content.js';
5
6
  import { createMarkdownProcessor, renderMarkdown } from './markdown.js';
@@ -10,6 +11,7 @@ import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '..
10
11
  import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
11
12
  import { getEnv } from '../env.js';
12
13
  import { DEFAULT_OUT_DIR } from '../constants.js';
14
+ import { createMetricRecorder, noopMetricRecorder } from '../metrics/index.js';
13
15
  /**
14
16
  * Recursively calculates the total size of a directory in bytes.
15
17
  * Used for build statistics.
@@ -103,19 +105,23 @@ const defaultLogger = {
103
105
  * 5. Update cache manifest
104
106
  *
105
107
  * @param options - Build configuration options
106
- * @returns Promise resolving to build statistics
108
+ * @returns Promise resolving to build result with statistics and optional metrics
107
109
  * @throws {Error} When configuration is invalid
108
110
  * @throws {Error} When template rendering fails
109
111
  * @throws {Error} When build lock cannot be acquired
110
112
  *
111
113
  * @example
112
114
  * ```typescript
113
- * const stats = await build({
115
+ * const result = await build({
114
116
  * force: true,
115
117
  * clean: true,
116
- * includeDrafts: false
118
+ * includeDrafts: false,
119
+ * metrics: { enabled: true }
117
120
  * });
118
- * console.log(`Built ${stats.pageCount} pages in ${stats.buildTime}ms`);
121
+ * console.log(`Built ${result.totalPages} pages in ${result.buildTimeMs}ms`);
122
+ * if (result.buildMetrics) {
123
+ * console.log(`Cache hit rate: ${result.buildMetrics.isg.cacheHitRate}`);
124
+ * }
119
125
  * ```
120
126
  */
121
127
  export async function build(options = {}) {
@@ -184,14 +190,16 @@ async function loadContentAndBuildNavigation(config, options, logger) {
184
190
  /**
185
191
  * Processes pages with ISG caching logic.
186
192
  */
187
- async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles) {
193
+ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder) {
188
194
  let cacheHits = 0;
189
195
  let cacheMisses = 0;
190
196
  // Build context
191
197
  const buildContext = { config, pages };
192
198
  // Run beforeAll hook
193
199
  if (config.hooks?.beforeAll) {
200
+ const endHookBeforeAll = recorder.startSpan('hookBeforeAllMs');
194
201
  await config.hooks.beforeAll(buildContext);
202
+ endHookBeforeAll();
195
203
  }
196
204
  // Render each page with tree-based progress tracking and ISG
197
205
  if (logger.step) {
@@ -239,22 +247,30 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
239
247
  else {
240
248
  logger.processing(`📋 Cached ${page.url}`);
241
249
  }
250
+ // Record page timing for cached pages (0ms render time)
251
+ recorder.recordPageTiming(page.url, 0, true);
242
252
  continue;
243
253
  }
244
254
  // Cache miss - need to rebuild
245
255
  cacheMisses++;
246
- const startTime = Date.now();
256
+ const startTime = performance.now();
247
257
  // Run beforeRender hook
248
258
  if (config.hooks?.beforeRender) {
259
+ const hookStart = performance.now();
249
260
  await config.hooks.beforeRender({ page, config });
261
+ recorder.addToPhase('hookBeforeRenderTotalMs', performance.now() - hookStart);
250
262
  }
251
- // Render markdown to HTML
252
- const htmlContent = renderMarkdown(page.content, md);
263
+ // Render markdown to HTML with TOC extraction
264
+ const tocEnabled = config.markdown?.toc !== false;
265
+ const { html: htmlContent, toc } = renderMarkdown(page.content, md, tocEnabled);
253
266
  // Compute matched bundle paths for this page
254
267
  const bundlePaths = getBundlePathsForPage(page.url, compiledBundles);
255
268
  const assets = bundlePaths.length > 0 ? { bundlePaths } : undefined;
256
269
  // Render with template
257
- let finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages, assets);
270
+ const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger);
271
+ let finalHtml = renderResult.html;
272
+ // Record templates loaded for this page
273
+ recorder.increment('templatesLoaded', renderResult.templatesLoaded);
258
274
  // Auto-inject SEO tags if enabled
259
275
  if (config.seo?.autoInject !== false) {
260
276
  const injectOptions = {
@@ -273,7 +289,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
273
289
  if (shouldAutoInject && assets && assets.bundlePaths.length > 0) {
274
290
  finalHtml = autoInjectBundles(finalHtml, assets.bundlePaths);
275
291
  }
276
- const renderTime = Date.now() - startTime;
292
+ const renderTime = Math.round(performance.now() - startTime);
293
+ // Record page timing for rendered pages (includes template count)
294
+ recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
277
295
  if (logger.updateTreeNode) {
278
296
  logger.updateTreeNode(pageId, 'completed', {
279
297
  timing: renderTime,
@@ -292,7 +310,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
292
310
  }
293
311
  // Run afterRender hook
294
312
  if (config.hooks?.afterRender) {
313
+ const hookStart = performance.now();
295
314
  await config.hooks.afterRender({ page, config });
315
+ recorder.addToPhase('hookAfterRenderTotalMs', performance.now() - hookStart);
296
316
  }
297
317
  }
298
318
  // Display final rendering tree and clear it
@@ -347,11 +367,29 @@ async function generateBuildStats(pages, assetsCount, buildStartTime, outDir, ca
347
367
  * Separated for cleaner error handling and testing.
348
368
  */
349
369
  async function buildInternal(options = {}) {
370
+ // Date.now() is used for user-facing build duration display (wall-clock time, in milliseconds).
371
+ // Note: Date.now() can be affected by system clock changes and is not monotonic.
372
+ // Internal metrics use performance.now() via the MetricRecorder for higher precision and monotonic timing,
373
+ // which is not affected by system clock adjustments.
350
374
  const buildStartTime = Date.now();
351
375
  const logger = options.logger || defaultLogger;
376
+ // Create metric recorder (noop if disabled)
377
+ const recorder = createMetricRecorder({
378
+ enabled: options.metrics?.enabled,
379
+ detailed: options.metrics?.detailed,
380
+ command: 'build',
381
+ flags: {
382
+ force: options.force,
383
+ clean: options.clean,
384
+ includeDrafts: options.includeDrafts,
385
+ },
386
+ statiVersion: options.version,
387
+ });
352
388
  logger.building('Building your site...');
353
- // Load configuration
389
+ // Load configuration (instrumented)
390
+ const endConfigSpan = recorder.startSpan('configLoadMs');
354
391
  const { config, outDir, cacheDir } = await loadAndValidateConfig(options);
392
+ endConfigSpan();
355
393
  // Initialize cache stats
356
394
  let cacheHits = 0;
357
395
  let cacheMisses = 0;
@@ -384,17 +422,27 @@ async function buildInternal(options = {}) {
384
422
  }
385
423
  }
386
424
  // Load cache manifest for ISG (after potential clean operation)
425
+ const endManifestLoadSpan = recorder.startSpan('cacheManifestLoadMs');
387
426
  const { manifest } = await setupCacheAndManifest(cacheDir);
427
+ endManifestLoadSpan();
388
428
  // Load content and build navigation
389
429
  if (logger.step) {
390
430
  console.log(); // Add spacing before content loading
391
431
  }
432
+ const endContentSpan = recorder.startSpan('contentDiscoveryMs');
392
433
  const { pages, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
434
+ endContentSpan();
435
+ // Record content discovery counts.
436
+ // Both totalPages and markdownFilesProcessed are incremented by pages.length
437
+ // because loadContent() only processes *.md files, so all pages are markdown files.
438
+ recorder.increment('totalPages', pages.length);
439
+ recorder.increment('markdownFilesProcessed', pages.length);
393
440
  // Store navigation hash in manifest for change detection in dev server
394
441
  manifest.navigationHash = navigationHash;
395
442
  // Compile TypeScript if enabled
396
443
  let compiledBundles = [];
397
444
  if (config.typescript?.enabled) {
445
+ const endTsSpan = recorder.startSpan('typescriptCompileMs');
398
446
  compiledBundles = await compileTypeScript({
399
447
  projectRoot: process.cwd(),
400
448
  config: config.typescript,
@@ -402,15 +450,21 @@ async function buildInternal(options = {}) {
402
450
  mode: getEnv() === 'production' ? 'production' : 'development',
403
451
  logger,
404
452
  });
453
+ endTsSpan();
405
454
  }
406
455
  // Process pages with ISG caching logic
407
456
  if (logger.step) {
408
457
  console.log(); // Add spacing before page processing
409
458
  }
459
+ const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
410
460
  const buildTime = new Date();
411
- const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles);
461
+ const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder);
462
+ endPageRenderSpan();
412
463
  cacheHits = pageProcessingResult.cacheHits;
413
464
  cacheMisses = pageProcessingResult.cacheMisses;
465
+ // Record page rendering counts
466
+ recorder.increment('renderedPages', cacheMisses);
467
+ recorder.increment('cachedPages', cacheHits);
414
468
  // Write Tailwind class inventory after all templates have been rendered (if Tailwind is used)
415
469
  if (hasTailwind) {
416
470
  const inventorySize = getInventorySize();
@@ -423,10 +477,15 @@ async function buildInternal(options = {}) {
423
477
  disableInventoryTracking();
424
478
  }
425
479
  // Save updated cache manifest
480
+ const endManifestSaveSpan = recorder.startSpan('cacheManifestSaveMs');
426
481
  await saveCacheManifest(cacheDir, manifest);
482
+ endManifestSaveSpan();
427
483
  // Copy static assets and count them
484
+ const endAssetSpan = recorder.startSpan('assetCopyMs');
428
485
  let assetsCount = 0;
429
486
  assetsCount = await copyStaticAssets(config, outDir, logger);
487
+ endAssetSpan();
488
+ recorder.increment('assetsCopied', assetsCount);
430
489
  // Get current environment
431
490
  const currentEnv = getEnv();
432
491
  // Generate sitemap if enabled (only in production mode)
@@ -435,8 +494,10 @@ async function buildInternal(options = {}) {
435
494
  console.log(); // Add spacing before sitemap generation
436
495
  }
437
496
  logger.info('Generating sitemap...');
497
+ const endSitemapSpan = recorder.startSpan('sitemapGenerationMs');
438
498
  const sitemapResult = generateSitemap(pages, config, config.sitemap);
439
499
  await writeFile(join(outDir, 'sitemap.xml'), sitemapResult.xml);
500
+ endSitemapSpan();
440
501
  // Write additional sitemap files if split
441
502
  if (sitemapResult.sitemaps) {
442
503
  for (const { filename, xml } of sitemapResult.sitemaps) {
@@ -506,9 +567,30 @@ async function buildInternal(options = {}) {
506
567
  }
507
568
  // Run afterAll hook
508
569
  if (config.hooks?.afterAll) {
570
+ const endHookAfterAll = recorder.startSpan('hookAfterAllMs');
509
571
  await config.hooks.afterAll({ config, pages });
572
+ endHookAfterAll();
510
573
  }
511
574
  // Calculate build statistics
512
575
  const buildStats = await generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger);
513
- return buildStats;
576
+ // Set ISG metrics
577
+ const totalPages = pages.length;
578
+ const cacheHitRate = totalPages > 0 ? cacheHits / totalPages : 0;
579
+ recorder.setISGMetrics({
580
+ enabled: config.isg?.enabled !== false,
581
+ cacheHitRate,
582
+ manifestEntries: Object.keys(manifest.entries).length,
583
+ invalidatedEntries: cacheMisses,
584
+ });
585
+ // Take final memory snapshot
586
+ recorder.snapshotMemory();
587
+ // Build result with optional metrics
588
+ const result = {
589
+ ...buildStats,
590
+ };
591
+ // Add metrics if enabled
592
+ if (recorder.enabled) {
593
+ result.buildMetrics = recorder.finalize();
594
+ }
595
+ return result;
514
596
  }
@@ -3,7 +3,7 @@
3
3
  * Barrel file for all core Stati functionality including build, dev server, preview, and invalidation.
4
4
  */
5
5
  export { build } from './build.js';
6
- export type { BuildOptions } from './build.js';
6
+ export type { BuildOptions, MetricsOptions, BuildResult } from './build.js';
7
7
  export { createDevServer } from './dev.js';
8
8
  export type { DevServerOptions } from './dev.js';
9
9
  export { createPreviewServer } from './preview.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG5E,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1,9 +1,26 @@
1
1
  import MarkdownIt from 'markdown-it';
2
- import type { StatiConfig } from '../types/index.js';
2
+ import type { StatiConfig, TocEntry } from '../types/index.js';
3
+ /**
4
+ * Result of rendering markdown content.
5
+ */
6
+ export interface MarkdownResult {
7
+ /** The rendered HTML content */
8
+ html: string;
9
+ /** Table of contents entries extracted from headings */
10
+ toc: TocEntry[];
11
+ }
3
12
  /**
4
13
  * Creates and configures a MarkdownIt processor based on the provided configuration.
5
14
  * Supports both plugin array format and configure function format.
6
15
  */
7
16
  export declare function createMarkdownProcessor(config: StatiConfig): Promise<MarkdownIt>;
8
- export declare function renderMarkdown(content: string, md: MarkdownIt): string;
17
+ /**
18
+ * Renders markdown content to HTML with optional TOC extraction.
19
+ *
20
+ * @param content - The markdown content to render
21
+ * @param md - The configured MarkdownIt instance
22
+ * @param tocEnabled - Whether to extract TOC and inject heading anchors (default: true)
23
+ * @returns Object containing rendered HTML and TOC entries
24
+ */
25
+ export declare function renderMarkdown(content: string, md: MarkdownIt, tocEnabled?: boolean): MarkdownResult;
9
26
  //# sourceMappingURL=markdown.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAIrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmBrD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuCtF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,MAAM,CAEtE"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,GAAG,EAAE,QAAQ,EAAE,CAAC;CACjB;AAmBD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuCtF;AA6ED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,UAAU,EACd,UAAU,GAAE,OAAc,GACzB,cAAc,CAWhB"}
@@ -2,6 +2,7 @@ import MarkdownIt from 'markdown-it';
2
2
  import { createRequire } from 'node:module';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import path from 'node:path';
5
+ import { slugify } from './utils/index.js';
5
6
  /**
6
7
  * Load a markdown plugin, trying different resolution strategies
7
8
  */
@@ -62,6 +63,84 @@ export async function createMarkdownProcessor(config) {
62
63
  }
63
64
  return md;
64
65
  }
65
- export function renderMarkdown(content, md) {
66
- return md.render(content);
66
+ /**
67
+ * Extracts text content from an inline token, handling nested children.
68
+ * Recursively processes all token types to capture text from links, images,
69
+ * code, and other inline elements.
70
+ *
71
+ * @param token - The inline token to extract text from
72
+ * @returns Plain text content
73
+ */
74
+ function extractTextFromToken(token) {
75
+ // Handle tokens with children by recursively extracting from all children
76
+ if (token.children && token.children.length > 0) {
77
+ return token.children.map((child) => extractTextFromToken(child)).join('');
78
+ }
79
+ // For leaf tokens, return their content
80
+ // This handles 'text', 'code_inline', and other inline token types
81
+ return token.content || '';
82
+ }
83
+ /**
84
+ * Extracts TOC entries from tokens and injects anchor IDs into heading tokens.
85
+ *
86
+ * @param tokens - The parsed markdown tokens
87
+ * @param tocEnabled - Whether TOC extraction is enabled
88
+ * @returns Array of TOC entries
89
+ */
90
+ function extractAndInjectAnchors(tokens, tocEnabled) {
91
+ if (!tocEnabled) {
92
+ return [];
93
+ }
94
+ const toc = [];
95
+ const usedIds = new Map();
96
+ for (let i = 0; i < tokens.length; i++) {
97
+ const token = tokens[i];
98
+ if (!token)
99
+ continue;
100
+ if (token.type === 'heading_open') {
101
+ // Extract level from tag (h1, h2, etc.)
102
+ const level = parseInt(token.tag.slice(1), 10);
103
+ // Only include levels 2-6 in TOC (skip h1)
104
+ if (level >= 2 && level <= 6) {
105
+ // Get the inline token that follows (contains heading text)
106
+ const inlineToken = tokens[i + 1];
107
+ if (inlineToken && inlineToken.type === 'inline') {
108
+ const text = extractTextFromToken(inlineToken);
109
+ let baseId = slugify(text);
110
+ // Fallback for empty slugs (e.g., headings with only emojis or special characters)
111
+ if (!baseId) {
112
+ baseId = 'heading';
113
+ }
114
+ // Handle duplicate IDs
115
+ let id = baseId;
116
+ const count = usedIds.get(baseId) || 0;
117
+ if (count > 0) {
118
+ id = `${baseId}-${count}`;
119
+ }
120
+ usedIds.set(baseId, count + 1);
121
+ // Inject the id attribute into the heading_open token
122
+ token.attrSet('id', id);
123
+ toc.push({ id, text, level });
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return toc;
129
+ }
130
+ /**
131
+ * Renders markdown content to HTML with optional TOC extraction.
132
+ *
133
+ * @param content - The markdown content to render
134
+ * @param md - The configured MarkdownIt instance
135
+ * @param tocEnabled - Whether to extract TOC and inject heading anchors (default: true)
136
+ * @returns Object containing rendered HTML and TOC entries
137
+ */
138
+ export function renderMarkdown(content, md, tocEnabled = true) {
139
+ // Parse content into tokens
140
+ const tokens = md.parse(content, {});
141
+ // Extract TOC and inject anchor IDs
142
+ const toc = extractAndInjectAnchors(tokens, tocEnabled);
143
+ // Render tokens to HTML
144
+ const html = md.renderer.render(tokens, md.options, {});
145
+ return { html, toc };
67
146
  }
@@ -1,5 +1,14 @@
1
1
  import { Eta } from 'eta';
2
- import type { StatiConfig, PageModel, NavNode } from '../types/index.js';
2
+ import type { StatiConfig, PageModel, NavNode, TocEntry, Logger } from '../types/index.js';
3
+ /**
4
+ * Result of rendering a page template.
5
+ */
6
+ export interface RenderResult {
7
+ /** The rendered HTML content */
8
+ html: string;
9
+ /** Number of templates loaded (layout + partials) */
10
+ templatesLoaded: number;
11
+ }
3
12
  export declare function createTemplateEngine(config: StatiConfig): Eta;
4
- export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[], assets?: import('../types/index.js').StatiAssets): Promise<string>;
13
+ export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[], assets?: import('../types/index.js').StatiAssets, toc?: TocEntry[], logger?: Logger): Promise<RenderResult>;
5
14
  //# sourceMappingURL=templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,mBAAmB,CAAC;AAmNzF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAW7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,GAC/C,OAAO,CAAC,MAAM,CAAC,CA+KjB"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAkB3B;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;CACzB;AAoMD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAW7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,YAAY,CAAC,CAuMvB"}
@@ -1,6 +1,7 @@
1
1
  import { Eta } from 'eta';
2
2
  import { join, dirname, relative, basename, posix } from 'node:path';
3
3
  import glob from 'fast-glob';
4
+ import { createFallbackLogger } from './utils/index.js';
4
5
  import { TEMPLATE_EXTENSION } from '../constants.js';
5
6
  import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, propValue, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
6
7
  import { getEnv } from '../env.js';
@@ -182,11 +183,14 @@ export function createTemplateEngine(config) {
182
183
  });
183
184
  return eta;
184
185
  }
185
- export async function renderPage(page, body, config, eta, navigation, allPages, assets) {
186
+ export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger) {
187
+ const log = logger || createFallbackLogger();
186
188
  // Discover partials for this page's directory hierarchy
187
189
  const srcDir = resolveSrcDir(config);
188
190
  const relativePath = relative(srcDir, page.sourcePath);
189
191
  const partialPaths = await discoverPartials(relativePath, config);
192
+ // Count templates: partials + layout (if found)
193
+ const partialsCount = Object.keys(partialPaths).length;
190
194
  // Build collection data based on page type
191
195
  let collectionData;
192
196
  const isIndexPage = allPages && isCollectionIndexPage(page, allPages);
@@ -203,19 +207,31 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
203
207
  // Discover the appropriate layout using hierarchical layout.eta convention
204
208
  // Pass isIndexPage flag to enable index.eta lookup for aggregation pages
205
209
  const layoutPath = await discoverLayout(relativePath, config, page.frontMatter.layout, isIndexPage);
210
+ // Calculate total templates loaded (partials + layout if present)
211
+ const templatesLoaded = partialsCount + (layoutPath ? 1 : 0);
206
212
  // Create navigation helpers
207
213
  const navTree = navigation || [];
208
214
  const navHelpers = createNavigationHelpers(navTree, page);
209
215
  const currentNavNode = navHelpers.getCurrentNode();
216
+ // Define page-specific properties that override frontmatter
217
+ const pageOverrides = {
218
+ path: page.url,
219
+ url: page.url, // Add url property for compatibility
220
+ content: body,
221
+ navNode: currentNavNode, // Add current page's navigation node
222
+ toc: toc || [], // Table of contents entries extracted from markdown headings
223
+ };
224
+ // Check for frontmatter keys that conflict with reserved page properties
225
+ const conflictingKeys = Object.keys(pageOverrides).filter((key) => key in page.frontMatter);
226
+ if (conflictingKeys.length > 0) {
227
+ log.warning(`Frontmatter in "${page.sourcePath}" contains reserved keys that will be overwritten: ${conflictingKeys.join(', ')}`);
228
+ }
210
229
  // Create base context for partial rendering
211
230
  const baseContext = {
212
231
  site: config.site,
213
232
  page: {
214
233
  ...page.frontMatter,
215
- path: page.url,
216
- url: page.url, // Add url property for compatibility
217
- content: body,
218
- navNode: currentNavNode, // Add current page's navigation node
234
+ ...pageOverrides,
219
235
  },
220
236
  content: body,
221
237
  nav: navHelpers, // Replace navigation with nav helpers
@@ -260,7 +276,7 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
260
276
  catch (error) {
261
277
  // If this is the last pass, log the error and create placeholder
262
278
  if (pass === maxPasses - 1) {
263
- console.warn(`Warning: Failed to render partial ${partialName} at ${partialPath}:`, error);
279
+ log.warning(`Failed to render partial ${partialName} at ${partialPath}: ${error instanceof Error ? error.message : String(error)}`);
264
280
  // In development mode, throw enhanced template error for partials too
265
281
  if (getEnv() === 'development') {
266
282
  const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), partialPath);
@@ -302,19 +318,20 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
302
318
  };
303
319
  try {
304
320
  if (!layoutPath) {
305
- console.warn('No layout template found, using fallback');
306
- return createFallbackHtml(page, body);
321
+ log.warning('No layout template found, using fallback');
322
+ return { html: createFallbackHtml(page, body), templatesLoaded };
307
323
  }
308
- return await eta.renderAsync(layoutPath, context);
324
+ const html = await eta.renderAsync(layoutPath, context);
325
+ return { html, templatesLoaded };
309
326
  }
310
327
  catch (error) {
311
- console.error(`Error rendering layout ${layoutPath || 'unknown'}:`, error);
328
+ log.error(`Error rendering layout ${layoutPath || 'unknown'}: ${error instanceof Error ? error.message : String(error)}`);
312
329
  // In development mode, throw enhanced template error for better debugging
313
330
  if (getEnv() === 'development') {
314
331
  const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), layoutPath || undefined);
315
332
  throw templateError;
316
333
  }
317
- return createFallbackHtml(page, body);
334
+ return { html: createFallbackHtml(page, body), templatesLoaded };
318
335
  }
319
336
  }
320
337
  function createFallbackHtml(page, body) {
@@ -6,6 +6,7 @@ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir,
6
6
  export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, } from './paths.utils.js';
7
7
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
8
8
  export { propValue } from './template.utils.js';
9
+ export { slugify } from './slugify.utils.js';
9
10
  export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.utils.js';
10
11
  export { createValidatingPartialsProxy } from './partial-validation.utils.js';
11
12
  export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.utils.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
@@ -10,6 +10,8 @@ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolv
10
10
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
11
11
  // Template utilities
12
12
  export { propValue } from './template.utils.js';
13
+ // Slugify utilities
14
+ export { slugify } from './slugify.utils.js';
13
15
  // Tailwind inventory utilities
14
16
  export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.utils.js';
15
17
  // Partial validation utilities