@stati/core 1.7.1 → 1.9.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.
@@ -352,8 +352,6 @@ async function buildInternal(options = {}) {
352
352
  logger.building('Building your site...');
353
353
  // Load configuration
354
354
  const { config, outDir, cacheDir } = await loadAndValidateConfig(options);
355
- // Load cache manifest for ISG
356
- const { manifest } = await setupCacheAndManifest(cacheDir);
357
355
  // Initialize cache stats
358
356
  let cacheHits = 0;
359
357
  let cacheMisses = 0;
@@ -364,6 +362,8 @@ async function buildInternal(options = {}) {
364
362
  await remove(cacheDir);
365
363
  }
366
364
  await ensureDir(outDir);
365
+ // Load cache manifest for ISG (after potential clean operation)
366
+ const { manifest } = await setupCacheAndManifest(cacheDir);
367
367
  // Load content and build navigation
368
368
  console.log(); // Add spacing before content loading
369
369
  const { pages, navigation, md, eta } = await loadContentAndBuildNavigation(config, options, logger);
@@ -244,6 +244,27 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
244
244
  }
245
245
  }
246
246
  }
247
+ // Look for Stati callable partial patterns: stati.partials.name( or stati.partials['name'](
248
+ // This catches both direct property access and bracket notation with or without arguments
249
+ // Patterns allow for optional whitespace before the opening parenthesis
250
+ const callablePartialPatterns = [
251
+ /stati\.partials\.(\w+)\s*\(/g, // stati.partials.header( or stati.partials.header (
252
+ /stati\.partials\[['"`]([^'"`]+)['"`]\]\s*\(/g, // stati.partials['header']( with whitespace
253
+ ];
254
+ for (const pattern of callablePartialPatterns) {
255
+ let match;
256
+ while ((match = pattern.exec(content)) !== null) {
257
+ const partialName = match[1];
258
+ if (partialName) {
259
+ // Resolve the partial by searching for it in underscore directories
260
+ const partialFileName = `${partialName}${TEMPLATE_EXTENSION}`;
261
+ const resolvedPath = await resolveTemplatePathInternal(partialFileName, srcDir, templateDir);
262
+ if (resolvedPath) {
263
+ dependencies.push(resolvedPath);
264
+ }
265
+ }
266
+ }
267
+ }
247
268
  return dependencies;
248
269
  }
249
270
  /**
@@ -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;AAuLzF,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,GACrB,OAAO,CAAC,MAAM,CAAC,CA6JjB"}
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;AAwLzF,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,GACrB,OAAO,CAAC,MAAM,CAAC,CAyKjB"}
@@ -2,7 +2,7 @@ import { Eta } from 'eta';
2
2
  import { join, dirname, relative, basename, posix } from 'path';
3
3
  import glob from 'fast-glob';
4
4
  import { TEMPLATE_EXTENSION } from '../constants.js';
5
- import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, propValue, } from './utils/index.js';
5
+ import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, propValue, wrapPartialsAsCallable, } from './utils/index.js';
6
6
  import { getEnv } from '../env.js';
7
7
  import { generateSEO } from '../seo/index.js';
8
8
  /**
@@ -223,9 +223,11 @@ export async function renderPage(page, body, config, eta, navigation, allPages)
223
223
  try {
224
224
  // Create context with all previously rendered partials available
225
225
  const combinedPartials = { ...renderedPartials, ...passRenderedPartials };
226
+ // Wrap partials as callable before passing to validation proxy
227
+ const callablePartials = wrapPartialsAsCallable(eta, combinedPartials, partialPaths, baseContext);
226
228
  const partialContext = {
227
229
  ...baseContext,
228
- partials: createValidatingPartialsProxy(combinedPartials), // Include both previous and current pass partials with validation
230
+ partials: createValidatingPartialsProxy(callablePartials), // Include both previous and current pass partials with validation
229
231
  };
230
232
  const renderedContent = await eta.renderAsync(partialPath, partialContext);
231
233
  passRenderedPartials[partialName] = renderedContent;
@@ -268,9 +270,11 @@ export async function renderPage(page, body, config, eta, navigation, allPages)
268
270
  break;
269
271
  }
270
272
  }
273
+ // Wrap final rendered partials as callable before passing to layout context
274
+ const callablePartials = wrapPartialsAsCallable(eta, renderedPartials, partialPaths, baseContext);
271
275
  const context = {
272
276
  ...baseContext,
273
- partials: createValidatingPartialsProxy(renderedPartials), // Add rendered partials with validation
277
+ partials: createValidatingPartialsProxy(callablePartials), // Add rendered partials with validation
274
278
  };
275
279
  try {
276
280
  if (!layoutPath) {
@@ -0,0 +1,60 @@
1
+ import { Eta } from 'eta';
2
+ /**
3
+ * Type definition for a callable partial function.
4
+ * Can be called with optional props or used directly as a value.
5
+ */
6
+ export type CallablePartial = {
7
+ (props?: Record<string, unknown>): string;
8
+ toString(): string;
9
+ valueOf(): string;
10
+ };
11
+ /**
12
+ * Creates a callable partial that can be used both as a value and as a function.
13
+ * This enables both syntaxes:
14
+ * - Direct usage: <%~ stati.partials.header %>
15
+ * - With props: <%~ stati.partials.hero({ title: 'Hello' }) %>
16
+ *
17
+ * @param eta - The Eta template engine instance
18
+ * @param partialPath - Absolute path to the partial template file
19
+ * @param baseContext - The base template context (without props)
20
+ * @param renderedContent - Pre-rendered content for the no-props case
21
+ * @returns A callable partial function
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const callable = makeCallablePartial(eta, '/path/to/partial.eta', baseContext, '<div>Header</div>');
26
+ *
27
+ * // Use without props (returns pre-rendered content)
28
+ * const html1 = callable.toString(); // '<div>Header</div>'
29
+ *
30
+ * // Use with props (re-renders with merged context)
31
+ * const html2 = callable({ title: 'Custom Title' }); // Renders with custom props
32
+ * ```
33
+ */
34
+ export declare function makeCallablePartial(eta: Eta, partialPath: string, baseContext: Record<string, unknown>, renderedContent: string): CallablePartial;
35
+ /**
36
+ * Wraps all partials in a record with callable partial wrappers.
37
+ * This allows partials to be used both as values and as functions.
38
+ *
39
+ * @param eta - The Eta template engine instance
40
+ * @param partials - Record mapping partial names to their rendered content
41
+ * @param partialPaths - Record mapping partial names to their absolute file paths
42
+ * @param baseContext - The base template context (without props)
43
+ * @returns Record of callable partials
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const callablePartials = wrapPartialsAsCallable(
48
+ * eta,
49
+ * { header: '<div>Header</div>', footer: '<div>Footer</div>' },
50
+ * { header: '/path/to/header.eta', footer: '/path/to/footer.eta' },
51
+ * baseContext
52
+ * );
53
+ *
54
+ * // Both syntaxes work
55
+ * callablePartials.header.toString(); // Direct usage
56
+ * callablePartials.header({ title: 'Custom' }); // With props
57
+ * ```
58
+ */
59
+ export declare function wrapPartialsAsCallable(eta: Eta, partials: Record<string, string>, partialPaths: Record<string, string>, baseContext: Record<string, unknown>): Record<string, CallablePartial>;
60
+ //# sourceMappingURL=callable-partials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callable-partials.d.ts","sourceRoot":"","sources":["../../../src/core/utils/callable-partials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IAC1C,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,IAAI,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,eAAe,EAAE,MAAM,GACtB,eAAe,CAqDjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAcjC"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Creates a callable partial that can be used both as a value and as a function.
3
+ * This enables both syntaxes:
4
+ * - Direct usage: <%~ stati.partials.header %>
5
+ * - With props: <%~ stati.partials.hero({ title: 'Hello' }) %>
6
+ *
7
+ * @param eta - The Eta template engine instance
8
+ * @param partialPath - Absolute path to the partial template file
9
+ * @param baseContext - The base template context (without props)
10
+ * @param renderedContent - Pre-rendered content for the no-props case
11
+ * @returns A callable partial function
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const callable = makeCallablePartial(eta, '/path/to/partial.eta', baseContext, '<div>Header</div>');
16
+ *
17
+ * // Use without props (returns pre-rendered content)
18
+ * const html1 = callable.toString(); // '<div>Header</div>'
19
+ *
20
+ * // Use with props (re-renders with merged context)
21
+ * const html2 = callable({ title: 'Custom Title' }); // Renders with custom props
22
+ * ```
23
+ */
24
+ export function makeCallablePartial(eta, partialPath, baseContext, renderedContent) {
25
+ /**
26
+ * The main callable function.
27
+ * When called with props, re-renders the partial with merged context.
28
+ * When called without props, returns the pre-rendered content.
29
+ */
30
+ const callable = (props) => {
31
+ if (!props || Object.keys(props).length === 0) {
32
+ // No props provided - return pre-rendered content
33
+ return renderedContent;
34
+ }
35
+ // Props provided - re-render with merged context
36
+ try {
37
+ const mergedContext = {
38
+ ...baseContext,
39
+ props, // Make props available as stati.props
40
+ };
41
+ // Render the partial with the merged context using renderAsync
42
+ // This is a synchronous call despite the name when used with already-loaded templates
43
+ const result = eta.render(partialPath, mergedContext);
44
+ return result || '';
45
+ }
46
+ catch (error) {
47
+ console.error(`Error rendering callable partial ${partialPath} with props:`, error);
48
+ return `<!-- Error rendering partial with props: ${error instanceof Error ? error.message : String(error)} -->`;
49
+ }
50
+ };
51
+ // Create a Proxy to handle different usage patterns
52
+ const proxy = new Proxy(callable, {
53
+ /**
54
+ * Handle function calls: stati.partials.header({ props })
55
+ */
56
+ apply(target, thisArg, args) {
57
+ return target.apply(thisArg, args);
58
+ },
59
+ /**
60
+ * Handle toString(): When used in template interpolation without parentheses
61
+ * Example: <%~ stati.partials.header %>
62
+ */
63
+ get(target, prop) {
64
+ if (prop === 'toString' || prop === 'valueOf') {
65
+ return () => renderedContent;
66
+ }
67
+ // Allow other function properties to pass through
68
+ return Reflect.get(target, prop);
69
+ },
70
+ });
71
+ return proxy;
72
+ }
73
+ /**
74
+ * Wraps all partials in a record with callable partial wrappers.
75
+ * This allows partials to be used both as values and as functions.
76
+ *
77
+ * @param eta - The Eta template engine instance
78
+ * @param partials - Record mapping partial names to their rendered content
79
+ * @param partialPaths - Record mapping partial names to their absolute file paths
80
+ * @param baseContext - The base template context (without props)
81
+ * @returns Record of callable partials
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const callablePartials = wrapPartialsAsCallable(
86
+ * eta,
87
+ * { header: '<div>Header</div>', footer: '<div>Footer</div>' },
88
+ * { header: '/path/to/header.eta', footer: '/path/to/footer.eta' },
89
+ * baseContext
90
+ * );
91
+ *
92
+ * // Both syntaxes work
93
+ * callablePartials.header.toString(); // Direct usage
94
+ * callablePartials.header({ title: 'Custom' }); // With props
95
+ * ```
96
+ */
97
+ export function wrapPartialsAsCallable(eta, partials, partialPaths, baseContext) {
98
+ const callablePartials = {};
99
+ for (const [name, renderedContent] of Object.entries(partials)) {
100
+ const partialPath = partialPaths[name];
101
+ if (!partialPath) {
102
+ console.warn(`No path found for partial "${name}", skipping callable wrapper`);
103
+ continue;
104
+ }
105
+ callablePartials[name] = makeCallablePartial(eta, partialPath, baseContext, renderedContent);
106
+ }
107
+ return callablePartials;
108
+ }
@@ -7,6 +7,8 @@ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolv
7
7
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.js';
8
8
  export { propValue } from './template-utils.js';
9
9
  export { createValidatingPartialsProxy } from './partial-validation.js';
10
+ export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.js';
11
+ export type { CallablePartial } from './callable-partials.js';
10
12
  export { TemplateError, parseEtaError, createTemplateError } from './template-errors.js';
11
13
  export { resolvePrettyUrl } from './server.js';
12
14
  export type { PrettyUrlResult } from './server.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,SAAS,CAAC;AAGjB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAGxE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,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,SAAS,CAAC;AAGjB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAGxE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrF,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
@@ -12,6 +12,8 @@ export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from
12
12
  export { propValue } from './template-utils.js';
13
13
  // Partial validation utilities
14
14
  export { createValidatingPartialsProxy } from './partial-validation.js';
15
+ // Callable partial utilities
16
+ export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.js';
15
17
  // Template error utilities
16
18
  export { TemplateError, parseEtaError, createTemplateError } from './template-errors.js';
17
19
  // Server utilities
@@ -1,6 +1,9 @@
1
+ import { type CallablePartial } from './callable-partials.js';
1
2
  /**
2
3
  * Creates a development-mode Proxy for the partials object that throws errors
3
- * when accessing non-existent partials instead of returning undefined
4
+ * when accessing non-existent partials instead of returning undefined.
5
+ *
6
+ * Supports both string partials and CallablePartial.
4
7
  */
5
- export declare function createValidatingPartialsProxy(partials: Record<string, string>): Record<string, string>;
8
+ export declare function createValidatingPartialsProxy<T extends string | CallablePartial>(partials: Record<string, T>): Record<string, T>;
6
9
  //# sourceMappingURL=partial-validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"partial-validation.d.ts","sourceRoot":"","sources":["../../../src/core/utils/partial-validation.ts"],"names":[],"mappings":"AA8FA;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmDxB"}
1
+ {"version":3,"file":"partial-validation.d.ts","sourceRoot":"","sources":["../../../src/core/utils/partial-validation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA8F9D;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,SAAS,MAAM,GAAG,eAAe,EAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAC1B,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CA+EnB"}
@@ -84,7 +84,9 @@ function findSimilarPartialNames(targetName, availableNames) {
84
84
  }
85
85
  /**
86
86
  * Creates a development-mode Proxy for the partials object that throws errors
87
- * when accessing non-existent partials instead of returning undefined
87
+ * when accessing non-existent partials instead of returning undefined.
88
+ *
89
+ * Supports both string partials and CallablePartial.
88
90
  */
89
91
  export function createValidatingPartialsProxy(partials) {
90
92
  // In production, return partials as-is
@@ -92,6 +94,11 @@ export function createValidatingPartialsProxy(partials) {
92
94
  if (getEnv() === 'production') {
93
95
  return partials;
94
96
  }
97
+ // If there are no partials, return the empty object as-is
98
+ // This avoids proxy-related issues during test serialization
99
+ if (Object.keys(partials).length === 0) {
100
+ return partials;
101
+ }
95
102
  return new Proxy(partials, {
96
103
  get(target, prop, receiver) {
97
104
  // Allow normal object operations
@@ -104,18 +111,39 @@ export function createValidatingPartialsProxy(partials) {
104
111
  return target[propName];
105
112
  }
106
113
  // Special case: allow accessing length, toString, etc.
107
- if (propName in Object.prototype || propName === 'length') {
114
+ // Also handle test framework inspection properties
115
+ if (propName in Object.prototype ||
116
+ propName === 'length' ||
117
+ propName === 'constructor' ||
118
+ propName === 'then' || // Promise detection
119
+ propName === '$$typeof' || // React inspection
120
+ propName === 'nodeType' || // DOM node detection
121
+ propName === 'asymmetricMatch' || // Jest/Vitest matcher
122
+ propName === 'toJSON' // JSON serialization
123
+ ) {
108
124
  return Reflect.get(target, prop, receiver);
109
125
  }
110
126
  // Property doesn't exist - return error overlay HTML instead of throwing
111
127
  const availablePartials = Object.keys(target);
112
128
  const suggestions = findSimilarPartialNames(propName, availablePartials);
113
- // Special case: throw error if no partials are available at all
114
- if (availablePartials.length === 0) {
115
- throw new Error('No partials are available');
116
- }
117
129
  // In development, render an inline error overlay
118
- return createInlineErrorOverlay(propName, suggestions);
130
+ const errorHtml = createInlineErrorOverlay(propName, suggestions);
131
+ // Check if we're dealing with CallablePartials by testing a known partial
132
+ const samplePartial = Object.values(target)[0];
133
+ const isCallable = typeof samplePartial === 'function';
134
+ if (isCallable) {
135
+ // For CallablePartial, return a function that returns the error HTML
136
+ // This prevents "string is not a function" errors when templates call missing partials
137
+ // Accept any arguments to handle props being passed
138
+ const errorFunction = (..._args) => errorHtml;
139
+ errorFunction.toString = () => errorHtml;
140
+ errorFunction.valueOf = () => errorHtml;
141
+ return errorFunction;
142
+ }
143
+ else {
144
+ // For string partials, return the error HTML directly
145
+ return errorHtml;
146
+ }
119
147
  },
120
148
  has(target, prop) {
121
149
  return prop in target;
@@ -86,8 +86,8 @@ export function autoInjectSEO(html, options) {
86
86
  // Inject SEO metadata before </head>
87
87
  const before = html.substring(0, headClosePos);
88
88
  const after = html.substring(headClosePos);
89
- // Add proper indentation (2 spaces) and newline
90
- const injected = `${before} ${seoMetadata}\n${after}`;
89
+ // Add proper indentation (4 spaces) and newline
90
+ const injected = `${before} ${seoMetadata}\n${after}`;
91
91
  logDebug(`Injected ${existingTags.size === 0 ? 'all' : 'missing'} SEO tags into ${page.url}`, {
92
92
  debug,
93
93
  config,
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/seo/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI7C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAsH3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAyE/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAuDjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE;IACP,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,EACD,IAAI,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,GAChC,MAAM,CA6CR"}
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/seo/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAS7C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAsH3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAyE/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAuDjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE;IACP,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,EACD,IAAI,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,GAChC,MAAM,CA6CR"}
@@ -3,7 +3,7 @@
3
3
  * Generates meta tags, Open Graph tags, Twitter Cards, and structured data
4
4
  */
5
5
  import { SEOTagType } from '../types/seo.js';
6
- import { escapeHtml, validateSEOMetadata, generateRobotsContent } from './utils/index.js';
6
+ import { escapeHtml, validateSEOMetadata, generateRobotsContent, resolveAbsoluteUrl, } from './utils/index.js';
7
7
  import { sanitizeStructuredData } from './utils/escape-and-validation.js';
8
8
  /**
9
9
  * Generate complete SEO metadata for a page.
@@ -94,7 +94,7 @@ export function generateSEOMetadata(ctx) {
94
94
  }
95
95
  // Canonical link
96
96
  if (shouldGenerate(SEOTagType.Canonical)) {
97
- const canonical = seo.canonical || `${siteUrl}${page.url}`;
97
+ const canonical = seo.canonical || resolveAbsoluteUrl(page.url || '/', siteUrl);
98
98
  meta.push(`<link rel="canonical" href="${escapeHtml(canonical)}">`);
99
99
  }
100
100
  // Robots meta tag
@@ -125,7 +125,7 @@ export function generateSEOMetadata(ctx) {
125
125
  const sanitized = sanitizeStructuredData(seo.structuredData, logger);
126
126
  meta.push(`<script type="application/ld+json">${JSON.stringify(sanitized)}</script>`);
127
127
  }
128
- return meta.join('\n ');
128
+ return meta.join('\n ');
129
129
  }
130
130
  /**
131
131
  * Generate Open Graph protocol meta tags.
@@ -148,7 +148,7 @@ export function generateOpenGraphTags(ctx) {
148
148
  // Basic OG tags with fallback chain
149
149
  const ogTitle = og.title || seo.title || page.frontMatter.title || config.site.title;
150
150
  const ogDescription = og.description || seo.description || page.frontMatter.description;
151
- const ogUrl = og.url || seo.canonical || `${siteUrl}${page.url}`;
151
+ const ogUrl = og.url || seo.canonical || resolveAbsoluteUrl(page.url || '/', siteUrl);
152
152
  const ogType = og.type || 'website';
153
153
  const ogSiteName = og.siteName || config.site.title;
154
154
  tags.push(`<meta property="og:title" content="${escapeHtml(ogTitle)}">`);
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA6ItD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,YAAY,GAAG,IAAI,CA4ErB;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAUlE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAatF;AAoBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,EAAE,EAClB,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,uBAAuB,CA8CzB"}
1
+ {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAqJtD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,YAAY,GAAG,IAAI,CA4ErB;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAUlE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAatF;AAoBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,EAAE,EAClB,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,uBAAuB,CA8CzB"}
@@ -122,8 +122,16 @@ function determinePriority(page, rules, defaultPriority = 0.5) {
122
122
  return validatePriority(rule.priority);
123
123
  }
124
124
  }
125
- else if (page.url === pattern || page.url.startsWith(pattern)) {
126
- return validatePriority(rule.priority);
125
+ else {
126
+ // For non-glob patterns, check exact match or path prefix
127
+ if (page.url === pattern) {
128
+ return validatePriority(rule.priority);
129
+ }
130
+ // For path prefix matching, ensure we match at path boundaries
131
+ // e.g., "/api" matches "/api/foo" but "/" only matches "/" exactly
132
+ if (pattern !== '/' && page.url.startsWith(pattern + '/')) {
133
+ return validatePriority(rule.priority);
134
+ }
127
135
  }
128
136
  }
129
137
  return defaultPriority;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stati/core",
3
- "version": "1.7.1",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",