@riebeckite/core 0.0.17 → 0.0.18

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.
package/README.md CHANGED
@@ -37,7 +37,7 @@ export default defineConfig({
37
37
  ```
38
38
 
39
39
  The content directory is resolved relative to the application root. See
40
- [Configuration](../../docs/en/docs/reference/configuration.md) for the full field list and the
40
+ [Configuration](../../docs/docs/reference/configuration.en.md) for the full field list and the
41
41
  `projectRoot` / `appRoot` / `configRoot` / `contentRoot` resolution rules.
42
42
 
43
43
  ## Public API
@@ -88,8 +88,8 @@ The content directory is resolved relative to the application root. See
88
88
 
89
89
  ## See also
90
90
 
91
- - [Framework Reference](../../docs/en/docs/reference/README.md)
92
- - [Content System](../../docs/en/docs/framework/content-system.md)
93
- - [Plugin System](../../docs/en/docs/reference/plugin-api.md) / [Theme System](../../docs/en/docs/reference/theme-api.md)
94
- - [Architecture](../../docs/en/docs/framework/architecture.md)
91
+ - [Framework Reference](../../docs/docs/reference/README.en.md)
92
+ - [Content System](../../docs/docs/framework/content-system.en.md)
93
+ - [Plugin System](../../docs/docs/reference/plugin-api.en.md) / [Theme System](../../docs/docs/reference/theme-api.en.md)
94
+ - [Architecture](../../docs/docs/framework/architecture.en.md)
95
95
 
package/README_ja.md CHANGED
@@ -29,7 +29,7 @@ export default defineConfig({
29
29
  });
30
30
  ```
31
31
 
32
- コンテンツディレクトリはアプリケーションルート基準で解決します。設定項目の全体と `projectRoot` / `appRoot` / `configRoot` / `contentRoot` の解決規則は [Configuration](../../docs/ja/docs/reference/configuration.md) を参照してください。
32
+ コンテンツディレクトリはアプリケーションルート基準で解決します。設定項目の全体と `projectRoot` / `appRoot` / `configRoot` / `contentRoot` の解決規則は [Configuration](../../docs/docs/reference/configuration.md) を参照してください。
33
33
 
34
34
  ## 公開 API
35
35
 
@@ -69,8 +69,8 @@ export default defineConfig({
69
69
 
70
70
  ## 関連資料
71
71
 
72
- - [Framework Reference](../../docs/ja/docs/reference/README.md)
73
- - [Content System](../../docs/ja/docs/framework/content-system.md)
74
- - [Plugin System](../../docs/ja/docs/reference/plugin-api.md) / [Theme System](../../docs/ja/docs/reference/theme-api.md)
75
- - [Architecture](../../docs/ja/docs/framework/architecture.md)
72
+ - [Framework Reference](../../docs/docs/reference/README.md)
73
+ - [Content System](../../docs/docs/framework/content-system.md)
74
+ - [Plugin System](../../docs/docs/reference/plugin-api.md) / [Theme System](../../docs/docs/reference/theme-api.md)
75
+ - [Architecture](../../docs/docs/framework/architecture.md)
76
76
 
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ export type { PluginCache } from "./src/plugin/plugin_cache.js";
42
42
  export { PluginDependencyError, type PluginDependencyErrorKind, } from "./src/plugin/plugin_dependency_error.js";
43
43
  export type { CreatePluginMemoOptions, PluginMemo, } from "./src/plugin/plugin_memo.js";
44
44
  export { createPluginMemo, stableStringify, } from "./src/plugin/plugin_memo.js";
45
+ export { resolveBuildOutputDirectory, resolveManagedStateRoot, } from "./src/project/project_layout.js";
45
46
  export type { ConfigValidationIssue, PluginOptionsValidator, } from "./src/types/config_validation.js";
46
47
  export type { ContentAsset, ContentBodySlot, ContentLink, ContentLinkKind, ContentLocationInput, ContentManifest, ContentManifestEntry, ContentManifestPluginClientEntry, ContentPublicLocation, ContentRedirect, FolderLocation, PluginPageRoute, } from "./src/types/content_manifest.js";
47
48
  export { appendContentBodySlot } from "./src/types/content_manifest.js";
@@ -71,6 +72,7 @@ export type { SiteConfig } from "./src/types/site_config.js";
71
72
  export type { RiebeckiteTheme, ThemeArticleLayoutPreset, ThemeAttributes, ThemeColorMode, ThemeConfig, ThemeDesignTokens, ThemeInput, ThemeStyle, ThemeTypographyPreset, } from "./src/types/theme_config.js";
72
73
  export { defineTheme } from "./src/types/theme_config.js";
73
74
  export { uniqueStrings } from "./src/utils/collections.js";
75
+ export { attachErrorPath } from "./src/utils/error.js";
74
76
  export { escapeHtml, escapeHtmlAttribute, escapeScriptJson, } from "./src/utils/html.js";
75
77
  export { normalizeTag } from "./src/utils/tags.js";
76
78
  export { calculateReadingTime, stripHtml } from "./src/utils/text.js";
package/dist/index.js CHANGED
@@ -119,41 +119,41 @@ function validateTheme(value, issues) {
119
119
  }
120
120
  }
121
121
  }
122
- function validateThemeConfig(value, path5, issues) {
123
- validateOptionalString(value.name, `${path5}.name`, issues);
122
+ function validateThemeConfig(value, path6, issues) {
123
+ validateOptionalString(value.name, `${path6}.name`, issues);
124
124
  validateOneOf(
125
125
  value.colorMode,
126
- `${path5}.colorMode`,
126
+ `${path6}.colorMode`,
127
127
  ["light", "dark", "system"],
128
128
  issues
129
129
  );
130
130
  validateOneOf(
131
131
  value.typography,
132
- `${path5}.typography`,
132
+ `${path6}.typography`,
133
133
  ["system", "serif", "sans"],
134
134
  issues
135
135
  );
136
136
  validateOneOf(
137
137
  value.articleLayout,
138
- `${path5}.articleLayout`,
138
+ `${path6}.articleLayout`,
139
139
  ["article", "sidebar", "full-width"],
140
140
  issues
141
141
  );
142
- validateStringArray(value.userCss, `${path5}.userCss`, issues);
142
+ validateStringArray(value.userCss, `${path6}.userCss`, issues);
143
143
  if (value.attributes !== void 0 && !isRecord(value.attributes)) {
144
- issues.push({ path: `${path5}.attributes`, message: "Expected an object." });
144
+ issues.push({ path: `${path6}.attributes`, message: "Expected an object." });
145
145
  }
146
146
  }
147
- function validateThemeStyles(value, path5, issues) {
147
+ function validateThemeStyles(value, path6, issues) {
148
148
  if (value === void 0) return;
149
149
  if (!Array.isArray(value)) {
150
- issues.push({ path: path5, message: "Expected an array." });
150
+ issues.push({ path: path6, message: "Expected an array." });
151
151
  return;
152
152
  }
153
153
  for (const [index, style] of value.entries()) {
154
154
  if (!isRecord(style) || !isNonEmptyString(style.moduleSpecifier)) {
155
155
  issues.push({
156
- path: `${path5}[${index}].moduleSpecifier`,
156
+ path: `${path6}[${index}].moduleSpecifier`,
157
157
  message: "Expected a non-empty string."
158
158
  });
159
159
  }
@@ -173,69 +173,69 @@ function validatePlugins(value, issues) {
173
173
  continue;
174
174
  }
175
175
  const plugin = input;
176
- const path5 = `plugins[${index}]`;
176
+ const path6 = `plugins[${index}]`;
177
177
  if (!isNonEmptyString(plugin.name)) {
178
178
  issues.push({
179
- path: `${path5}.name`,
179
+ path: `${path6}.name`,
180
180
  message: "Expected a non-empty string."
181
181
  });
182
182
  } else if (plugin.enabled !== false) {
183
183
  const duplicateIndex = names.get(plugin.name);
184
184
  if (duplicateIndex !== void 0) {
185
185
  issues.push({
186
- path: `${path5}.name`,
186
+ path: `${path6}.name`,
187
187
  message: `Duplicate plugin name "${plugin.name}" (also plugins[${duplicateIndex}]).`
188
188
  });
189
189
  } else {
190
190
  names.set(plugin.name, index);
191
191
  }
192
192
  }
193
- validateOptionalBoolean(plugin.enabled, `${path5}.enabled`, issues);
194
- validateCapabilityMetadata(plugin, path5, issues);
195
- if (plugin.enabled !== false) validatePluginOptions(plugin, path5, issues);
193
+ validateOptionalBoolean(plugin.enabled, `${path6}.enabled`, issues);
194
+ validateCapabilityMetadata(plugin, path6, issues);
195
+ if (plugin.enabled !== false) validatePluginOptions(plugin, path6, issues);
196
196
  }
197
197
  }
198
- function validateCapabilityMetadata(plugin, path5, issues) {
199
- validateStringArray(plugin.provides, `${path5}.provides`, issues, true);
200
- validateStringArray(plugin.requires, `${path5}.requires`, issues, true);
201
- validateStringArray(plugin.optional, `${path5}.optional`, issues, true);
198
+ function validateCapabilityMetadata(plugin, path6, issues) {
199
+ validateStringArray(plugin.provides, `${path6}.provides`, issues, true);
200
+ validateStringArray(plugin.requires, `${path6}.requires`, issues, true);
201
+ validateStringArray(plugin.optional, `${path6}.optional`, issues, true);
202
202
  }
203
- function validatePluginOptions(plugin, path5, issues) {
203
+ function validatePluginOptions(plugin, path6, issues) {
204
204
  if (!plugin.validateOptions) return;
205
205
  try {
206
206
  const optionIssues = plugin.validateOptions(plugin.options) ?? [];
207
207
  for (const issue of optionIssues) {
208
208
  issues.push({
209
- path: issue.path ? `${path5}.options.${issue.path}` : `${path5}.options`,
209
+ path: issue.path ? `${path6}.options.${issue.path}` : `${path6}.options`,
210
210
  message: issue.message
211
211
  });
212
212
  }
213
213
  } catch (error) {
214
214
  issues.push({
215
- path: `${path5}.options`,
215
+ path: `${path6}.options`,
216
216
  message: `Validator failed: ${toErrorMessage(error)}`
217
217
  });
218
218
  }
219
219
  }
220
- function validateRequiredString(value, path5, issues) {
220
+ function validateRequiredString(value, path6, issues) {
221
221
  if (!isNonEmptyString(value)) {
222
- issues.push({ path: path5, message: "Expected a non-empty string." });
222
+ issues.push({ path: path6, message: "Expected a non-empty string." });
223
223
  }
224
224
  }
225
- function validateOptionalString(value, path5, issues) {
225
+ function validateOptionalString(value, path6, issues) {
226
226
  if (value !== void 0 && typeof value !== "string") {
227
- issues.push({ path: path5, message: "Expected a string." });
227
+ issues.push({ path: path6, message: "Expected a string." });
228
228
  }
229
229
  }
230
- function validateOptionalBoolean(value, path5, issues) {
230
+ function validateOptionalBoolean(value, path6, issues) {
231
231
  if (value !== void 0 && typeof value !== "boolean") {
232
- issues.push({ path: path5, message: "Expected a boolean." });
232
+ issues.push({ path: path6, message: "Expected a boolean." });
233
233
  }
234
234
  }
235
- function validateAbsoluteUrl(value, path5, issues) {
235
+ function validateAbsoluteUrl(value, path6, issues) {
236
236
  if (value === void 0 || value === "") return;
237
237
  if (typeof value !== "string") {
238
- issues.push({ path: path5, message: "Expected an absolute URL." });
238
+ issues.push({ path: path6, message: "Expected an absolute URL." });
239
239
  return;
240
240
  }
241
241
  try {
@@ -243,27 +243,27 @@ function validateAbsoluteUrl(value, path5, issues) {
243
243
  if (url.protocol !== "http:" && url.protocol !== "https:")
244
244
  throw new Error();
245
245
  } catch {
246
- issues.push({ path: path5, message: "Expected an absolute HTTP(S) URL." });
246
+ issues.push({ path: path6, message: "Expected an absolute HTTP(S) URL." });
247
247
  }
248
248
  }
249
- function validateStringArray(value, path5, issues, nonEmpty = false) {
249
+ function validateStringArray(value, path6, issues, nonEmpty = false) {
250
250
  if (value === void 0) return;
251
251
  if (!Array.isArray(value)) {
252
- issues.push({ path: path5, message: "Expected an array." });
252
+ issues.push({ path: path6, message: "Expected an array." });
253
253
  return;
254
254
  }
255
255
  for (const [index, item] of value.entries()) {
256
256
  if (typeof item !== "string" || nonEmpty && !item.trim()) {
257
257
  issues.push({
258
- path: `${path5}[${index}]`,
258
+ path: `${path6}[${index}]`,
259
259
  message: nonEmpty ? "Expected a non-empty string." : "Expected a string."
260
260
  });
261
261
  }
262
262
  }
263
263
  }
264
- function validateOneOf(value, path5, choices, issues) {
264
+ function validateOneOf(value, path6, choices, issues) {
265
265
  if (value !== void 0 && (typeof value !== "string" || !choices.includes(value))) {
266
- issues.push({ path: path5, message: `Expected one of: ${choices.join(", ")}.` });
266
+ issues.push({ path: path6, message: `Expected one of: ${choices.join(", ")}.` });
267
267
  }
268
268
  }
269
269
  function formatValidationIssues(issues) {
@@ -284,6 +284,74 @@ function toErrorMessage(error) {
284
284
  return error instanceof Error ? error.message : String(error);
285
285
  }
286
286
 
287
+ // src/types/publish_strategy.ts
288
+ function isPublishable(strategy, frontmatter) {
289
+ if (strategy === "explicit") {
290
+ return frontmatter?.publish === true;
291
+ }
292
+ return !(frontmatter?.private === true || frontmatter?.draft === true);
293
+ }
294
+
295
+ // src/content/publishing.ts
296
+ function resolvePublishingState(frontmatter, options) {
297
+ const explicitVisibility = resolveExplicitVisibility(frontmatter?.visibility);
298
+ const publishAt = resolvePublishAt(frontmatter?.publishAt);
299
+ if (explicitVisibility === "draft")
300
+ return state("draft", false, false, publishAt);
301
+ if (publishAt && publishAt.getTime() > options.buildTime.getTime()) {
302
+ return state("scheduled", false, false, publishAt);
303
+ }
304
+ if (explicitVisibility === "unlisted") {
305
+ return state("unlisted", true, false, publishAt);
306
+ }
307
+ if (explicitVisibility === "public" || publishAt) {
308
+ return state("public", true, true, publishAt);
309
+ }
310
+ const routable = isPublishable(options.strategy, frontmatter);
311
+ return state(routable ? "public" : "draft", routable, routable, publishAt);
312
+ }
313
+ function resolvePublishingBuildTime(input) {
314
+ if (input === void 0) return /* @__PURE__ */ new Date();
315
+ if (input instanceof Date) {
316
+ if (Number.isNaN(input.getTime()))
317
+ throw new Error("Invalid publishing build time.");
318
+ return new Date(input.getTime());
319
+ }
320
+ const parsed = new Date(input);
321
+ if (Number.isNaN(parsed.getTime()))
322
+ throw new Error("Invalid publishing build time.");
323
+ return parsed;
324
+ }
325
+ function resolveExplicitVisibility(value) {
326
+ if (value === void 0) return void 0;
327
+ if (value === "public" || value === "unlisted" || value === "draft")
328
+ return value;
329
+ throw new Error(
330
+ `Invalid content visibility "${String(value)}". Expected public, unlisted, or draft.`
331
+ );
332
+ }
333
+ function resolvePublishAt(value) {
334
+ if (value === void 0) return void 0;
335
+ if (!(typeof value === "string" || value instanceof Date)) {
336
+ throw new Error("Invalid publishAt. Expected an ISO 8601 date string.");
337
+ }
338
+ const parsed = value instanceof Date ? value : new Date(value);
339
+ if (Number.isNaN(parsed.getTime())) {
340
+ throw new Error(
341
+ `Invalid publishAt "${String(value)}". Expected an ISO 8601 date string.`
342
+ );
343
+ }
344
+ return new Date(parsed.getTime());
345
+ }
346
+ function state(visibility, routable, discoverable, publishAt) {
347
+ return {
348
+ visibility,
349
+ routable,
350
+ discoverable,
351
+ ...publishAt ? { publishAt } : {}
352
+ };
353
+ }
354
+
287
355
  // src/plugin/plugin_dependency_error.ts
288
356
  var PluginDependencyError = class extends Error {
289
357
  kind;
@@ -573,14 +641,6 @@ function getResolvedPluginMetadata(plugins) {
573
641
  }));
574
642
  }
575
643
 
576
- // src/types/publish_strategy.ts
577
- function isPublishable(strategy, frontmatter) {
578
- if (strategy === "explicit") {
579
- return frontmatter?.publish === true;
580
- }
581
- return !(frontmatter?.private === true || frontmatter?.draft === true);
582
- }
583
-
584
644
  // src/types/theme_config.ts
585
645
  function defineTheme(theme) {
586
646
  return theme;
@@ -680,10 +740,11 @@ function sanitizeThemeAttributes(attributes) {
680
740
  ).filter(([name]) => !reservedAttributes.has(name))
681
741
  );
682
742
  }
683
- function isPublished(config, frontmatter) {
684
- if (frontmatter?.visibility === "unlisted") return true;
685
- if (frontmatter?.visibility === "draft") return false;
686
- return isPublishable(config.content.filters.publishStrategy, frontmatter);
743
+ function isPublished(config, frontmatter, options = {}) {
744
+ return resolvePublishingState(frontmatter, {
745
+ strategy: config.content.filters.publishStrategy,
746
+ buildTime: resolvePublishingBuildTime(options.buildTime)
747
+ }).routable;
687
748
  }
688
749
  function isExcluded(patterns, relativePath) {
689
750
  const normalized = relativePath.replace(/\\/g, "/");
@@ -745,9 +806,9 @@ var configDeprecations = [];
745
806
  function collectConfigDeprecationDiagnostics(config, notices = configDeprecations) {
746
807
  return notices.filter((notice) => hasConfigPath(config, notice.path)).map(createDeprecationDiagnostic);
747
808
  }
748
- function hasConfigPath(config, path5) {
809
+ function hasConfigPath(config, path6) {
749
810
  let value = config;
750
- for (const segment of path5.split(".")) {
811
+ for (const segment of path6.split(".")) {
751
812
  if (!isRecord2(value) || !(segment in value)) return false;
752
813
  value = value[segment];
753
814
  }
@@ -810,7 +871,7 @@ import fs from "node:fs/promises";
810
871
  import path from "node:path";
811
872
 
812
873
  // src/content/content_build_state.ts
813
- var CONTENT_BUILD_STATE_VERSION = 7;
874
+ var CONTENT_BUILD_STATE_VERSION = 8;
814
875
  var CONTENT_BUILD_STATE_DIRECTORY = ".riebeckite";
815
876
  var CONTENT_BUILD_STATE_EXCLUDE = `${CONTENT_BUILD_STATE_DIRECTORY}/**`;
816
877
 
@@ -1215,9 +1276,9 @@ function buildContentCollections(entries, definitions) {
1215
1276
  function normalizePageSize(value) {
1216
1277
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1217
1278
  }
1218
- function withPage(path5, index) {
1219
- if (index === 0) return path5;
1220
- const base = path5 === "/" ? "" : path5;
1279
+ function withPage(path6, index) {
1280
+ if (index === 0) return path6;
1281
+ const base = path6 === "/" ? "" : path6;
1221
1282
  return normalizePath(`${base}/page/${index + 1}`);
1222
1283
  }
1223
1284
  function defaultPath(context) {
@@ -2463,9 +2524,9 @@ function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
2463
2524
  }
2464
2525
  return normalizedParts.join("/").replace(/\.md$/i, "");
2465
2526
  }
2466
- function decodeUriPath(path5) {
2527
+ function decodeUriPath(path6) {
2467
2528
  try {
2468
- return decodeURI(path5);
2529
+ return decodeURI(path6);
2469
2530
  } catch {
2470
2531
  return null;
2471
2532
  }
@@ -2553,16 +2614,16 @@ var GeneratedOutputRegistry = class {
2553
2614
  outputs = [];
2554
2615
  owners = /* @__PURE__ */ new Map();
2555
2616
  sinkFor(pluginName) {
2556
- const register = (path5, output) => {
2557
- const owner = this.owners.get(path5);
2617
+ const register = (path6, output) => {
2618
+ const owner = this.owners.get(path6);
2558
2619
  if (owner) {
2559
2620
  throw new Error(
2560
- `Duplicate generated output path "${path5}" declared by "${owner}" and "${pluginName}".`
2621
+ `Duplicate generated output path "${path6}" declared by "${owner}" and "${pluginName}".`
2561
2622
  );
2562
2623
  }
2563
- this.owners.set(path5, pluginName);
2624
+ this.owners.set(path6, pluginName);
2564
2625
  this.outputs.push({
2565
- path: path5,
2626
+ path: path6,
2566
2627
  content: output.content,
2567
2628
  dependencies: output.dependencies,
2568
2629
  owner: pluginName
@@ -2586,9 +2647,12 @@ var GeneratedOutputRegistry = class {
2586
2647
 
2587
2648
  // src/plugin/plugin_lifecycle.ts
2588
2649
  var PluginHookError = class extends Error {
2650
+ path;
2589
2651
  constructor(pluginName, hookName, cause) {
2590
2652
  super(`Plugin "${pluginName}" failed during "${hookName}"`, { cause });
2591
2653
  this.name = "PluginHookError";
2654
+ const path6 = cause?.path;
2655
+ if (typeof path6 === "string") this.path = path6;
2592
2656
  }
2593
2657
  };
2594
2658
  async function runPluginHook(plugin, hookName, context, hook) {
@@ -3073,12 +3137,12 @@ function normalizePageTypePath(pathname, pageType) {
3073
3137
  return pageType.directoryIndex ? normalizeDirectoryPagePath(pathname) : normalizePagePath(pathname);
3074
3138
  }
3075
3139
  function normalizeDirectoryPagePath(pathname) {
3076
- const path5 = normalizePagePath(pathname);
3077
- return path5 === "/" ? path5 : `${path5}/`;
3140
+ const path6 = normalizePagePath(pathname);
3141
+ return path6 === "/" ? path6 : `${path6}/`;
3078
3142
  }
3079
3143
  function normalizePagePath(pathname) {
3080
- const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3081
- return path5 === "/" ? path5 : path5.replace(/\/$/, "");
3144
+ const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3145
+ return path6 === "/" ? path6 : path6.replace(/\/$/, "");
3082
3146
  }
3083
3147
  function countSeverity(diagnostics, severity) {
3084
3148
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
@@ -3101,6 +3165,14 @@ function uniqueOutputs(outputs) {
3101
3165
  return [...byPath.values()].map(({ output }) => output).sort((left, right) => left.path.localeCompare(right.path));
3102
3166
  }
3103
3167
 
3168
+ // src/utils/error.ts
3169
+ function attachErrorPath(error, path6) {
3170
+ if (error instanceof Error && error.path === void 0) {
3171
+ error.path = path6;
3172
+ }
3173
+ return error;
3174
+ }
3175
+
3104
3176
  // src/content/affected_content.ts
3105
3177
  function determineAffectedContent(changeSet, previousState, currentPaths, currentContentIndex) {
3106
3178
  const changedPaths = [
@@ -3121,9 +3193,9 @@ function determineAffectedContent(changeSet, previousState, currentPaths, curren
3121
3193
  previousState.contentIndex,
3122
3194
  currentContentIndex
3123
3195
  );
3124
- for (const [path5, entry] of Object.entries(previousState.entries)) {
3125
- if (!isMarkdownPath2(path5)) continue;
3126
- const slug = toDependencyId(path5);
3196
+ for (const [path6, entry] of Object.entries(previousState.entries)) {
3197
+ if (!isMarkdownPath2(path6)) continue;
3198
+ const slug = toDependencyId(path6);
3127
3199
  if (direct.has(slug) || dependent.has(slug)) continue;
3128
3200
  if (!entry.linkTargets?.some((target) => changedIndexKeys.has(target))) {
3129
3201
  continue;
@@ -3160,27 +3232,27 @@ function findChangedIndexKeys(previousIndex, currentIndex) {
3160
3232
  return changed;
3161
3233
  }
3162
3234
  function addAllNotes(target, currentPaths, exclude) {
3163
- for (const path5 of currentPaths) {
3164
- if (!isMarkdownPath2(path5)) continue;
3165
- const slug = toDependencyId(path5);
3235
+ for (const path6 of currentPaths) {
3236
+ if (!isMarkdownPath2(path6)) continue;
3237
+ const slug = toDependencyId(path6);
3166
3238
  if (exclude?.has(slug)) continue;
3167
3239
  target.add(slug);
3168
3240
  }
3169
3241
  }
3170
- function isMarkdownPath2(path5) {
3171
- return path5.endsWith(".md");
3242
+ function isMarkdownPath2(path6) {
3243
+ return path6.endsWith(".md");
3172
3244
  }
3173
- function toDependencyId(path5) {
3174
- return path5.replace(/\.md$/, "");
3245
+ function toDependencyId(path6) {
3246
+ return path6.replace(/\.md$/, "");
3175
3247
  }
3176
- function toDependency(path5) {
3177
- return isMarkdownPath2(path5) ? { kind: "content", id: toDependencyId(path5) } : { kind: "file", id: path5.replace(/\\/g, "/") };
3248
+ function toDependency(path6) {
3249
+ return isMarkdownPath2(path6) ? { kind: "content", id: toDependencyId(path6) } : { kind: "file", id: path6.replace(/\\/g, "/") };
3178
3250
  }
3179
3251
  function buildDependentsByDependency(state2) {
3180
3252
  const dependents = /* @__PURE__ */ new Map();
3181
- for (const [path5, entry] of Object.entries(state2.entries)) {
3182
- if (!isMarkdownPath2(path5)) continue;
3183
- const slug = toDependencyId(path5);
3253
+ for (const [path6, entry] of Object.entries(state2.entries)) {
3254
+ if (!isMarkdownPath2(path6)) continue;
3255
+ const slug = toDependencyId(path6);
3184
3256
  for (const dependency of entry.dependencies) {
3185
3257
  const key = dependencyKey2(dependency);
3186
3258
  const values = dependents.get(key) ?? /* @__PURE__ */ new Set();
@@ -3202,18 +3274,18 @@ function diffContentEntries(previous, current) {
3202
3274
  const added = [];
3203
3275
  const changed = [];
3204
3276
  const unchanged = [];
3205
- for (const [path5, fingerprint] of currentFingerprints) {
3206
- const previousEntry = previous.entries[path5];
3277
+ for (const [path6, fingerprint] of currentFingerprints) {
3278
+ const previousEntry = previous.entries[path6];
3207
3279
  if (!previousEntry) {
3208
- added.push(path5);
3280
+ added.push(path6);
3209
3281
  } else if (previousEntry.fingerprint !== fingerprint) {
3210
- changed.push(path5);
3282
+ changed.push(path6);
3211
3283
  } else {
3212
- unchanged.push(path5);
3284
+ unchanged.push(path6);
3213
3285
  }
3214
3286
  }
3215
3287
  const removed = Object.keys(previous.entries).filter(
3216
- (path5) => !currentFingerprints.has(path5)
3288
+ (path6) => !currentFingerprints.has(path6)
3217
3289
  );
3218
3290
  return sortChangeSet({ added, changed, removed, unchanged });
3219
3291
  }
@@ -3225,9 +3297,6 @@ function allContentChanged(entries) {
3225
3297
  unchanged: []
3226
3298
  });
3227
3299
  }
3228
- function hasContentChanges(changeSet) {
3229
- return changeSet.added.length > 0 || changeSet.changed.length > 0 || changeSet.removed.length > 0;
3230
- }
3231
3300
  function sortChangeSet(changeSet) {
3232
3301
  return {
3233
3302
  added: [...changeSet.added].sort(),
@@ -3303,54 +3372,73 @@ var ContentIndexBuilder = class _ContentIndexBuilder {
3303
3372
  }
3304
3373
  source;
3305
3374
  async build(contentEntries, read = (entry) => this.source.read(entry)) {
3306
- const index = /* @__PURE__ */ new Map();
3375
+ const candidates = /* @__PURE__ */ new Map();
3307
3376
  for (const contentEntry of contentEntries) {
3308
- await this.indexContentEntry(index, contentEntry, read);
3377
+ await this.indexContentEntry(candidates, contentEntry, read);
3309
3378
  }
3310
- return index;
3379
+ return resolveContentIndex(candidates);
3311
3380
  }
3312
3381
  static buildFromAliases(contentEntries, aliasesByPath) {
3313
- const index = /* @__PURE__ */ new Map();
3382
+ const candidates = /* @__PURE__ */ new Map();
3314
3383
  for (const contentEntry of contentEntries) {
3315
3384
  _ContentIndexBuilder.indexContentEntryFromAliases(
3316
- index,
3385
+ candidates,
3317
3386
  contentEntry,
3318
3387
  aliasesByPath.get(contentEntry.path) ?? []
3319
3388
  );
3320
3389
  }
3321
- return index;
3390
+ return resolveContentIndex(candidates);
3322
3391
  }
3323
- async indexContentEntry(index, contentEntry, read) {
3392
+ async indexContentEntry(candidates, contentEntry, read) {
3324
3393
  const aliases = contentEntry.path.toLowerCase().endsWith(".md") ? extractFrontmatterAliases(readText(await read(contentEntry))) : [];
3325
3394
  _ContentIndexBuilder.indexContentEntryFromAliases(
3326
- index,
3395
+ candidates,
3327
3396
  contentEntry,
3328
3397
  aliases
3329
3398
  );
3330
3399
  }
3331
- static indexContentEntryFromAliases(index, contentEntry, aliases) {
3400
+ static indexContentEntryFromAliases(candidates, contentEntry, aliases) {
3332
3401
  const contentPath = contentEntry.path;
3333
3402
  const ext = contentPath.split(".").pop()?.toLowerCase() ?? "";
3334
3403
  const value = ext === "md" ? contentPath.replace(/\.md$/, "") : contentPath;
3335
3404
  const parts = value.split("/");
3336
3405
  for (let i = parts.length - 1; i >= 0; i--) {
3337
3406
  const rawSuffix = parts.slice(i).join("/");
3338
- addIndexEntry(index, rawSuffix, value);
3407
+ addCandidate(candidates, rawSuffix, value);
3339
3408
  if (ext !== "md") {
3340
- addIndexEntry(index, rawSuffix.replace(/\.[^/.]+$/, ""), value);
3409
+ addCandidate(candidates, rawSuffix.replace(/\.[^/.]+$/, ""), value);
3341
3410
  }
3342
3411
  }
3343
3412
  if (ext === "md") {
3344
- for (const alias of aliases) addIndexEntry(index, alias, value);
3413
+ for (const alias of aliases) addCandidate(candidates, alias, value);
3345
3414
  }
3346
3415
  }
3347
3416
  };
3417
+ function resolveContentIndex(candidates) {
3418
+ const index = /* @__PURE__ */ new Map();
3419
+ const ambiguities = /* @__PURE__ */ new Map();
3420
+ for (const [key, values] of candidates) {
3421
+ const list = [...values].sort(compareCandidates);
3422
+ const [only] = list;
3423
+ if (list.length === 1 && only !== void 0) {
3424
+ index.set(key, only);
3425
+ continue;
3426
+ }
3427
+ ambiguities.set(key, list);
3428
+ }
3429
+ return { index, ambiguities };
3430
+ }
3431
+ function compareCandidates(left, right) {
3432
+ return left < right ? -1 : left > right ? 1 : 0;
3433
+ }
3348
3434
  function readText(content) {
3349
3435
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3350
3436
  }
3351
- function addIndexEntry(index, key, value) {
3437
+ function addCandidate(candidates, key, value) {
3352
3438
  const normalizedKey = key.toLowerCase();
3353
- if (!index.has(normalizedKey)) index.set(normalizedKey, value);
3439
+ const values = candidates.get(normalizedKey) ?? /* @__PURE__ */ new Set();
3440
+ values.add(value);
3441
+ candidates.set(normalizedKey, values);
3354
3442
  }
3355
3443
 
3356
3444
  // src/content/content_build_coordinator.ts
@@ -3433,7 +3521,10 @@ var ContentBuildCoordinator = class {
3433
3521
  previousState,
3434
3522
  changeSet
3435
3523
  );
3436
- const currentContentIndex = await this.dependencies.observability.tracer.span(
3524
+ const {
3525
+ index: currentContentIndex,
3526
+ ambiguities: currentContentIndexAmbiguities
3527
+ } = await this.dependencies.observability.tracer.span(
3437
3528
  "content.index",
3438
3529
  {},
3439
3530
  () => ContentIndexBuilder.buildFromAliases(
@@ -3471,6 +3562,7 @@ var ContentBuildCoordinator = class {
3471
3562
  currentEntries,
3472
3563
  currentContentAliases,
3473
3564
  currentContentIndex,
3565
+ currentContentIndexAmbiguities,
3474
3566
  changeSet,
3475
3567
  affectedContent: affected
3476
3568
  };
@@ -3498,8 +3590,8 @@ var ContentBuildCoordinator = class {
3498
3590
  function readText2(content) {
3499
3591
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3500
3592
  }
3501
- function toSlug(path5) {
3502
- return path5.replace(/\.md$/, "");
3593
+ function toSlug(path6) {
3594
+ return path6.replace(/\.md$/, "");
3503
3595
  }
3504
3596
  function collectDependencies2(manifestEntry, trackedDependencies) {
3505
3597
  if (!manifestEntry) return [];
@@ -3682,8 +3774,8 @@ var ContentLocationResolver = class {
3682
3774
  }
3683
3775
  }
3684
3776
  };
3685
- function toSlug2(path5) {
3686
- return path5.replace(/\.md$/, "");
3777
+ function toSlug2(path6) {
3778
+ return path6.replace(/\.md$/, "");
3687
3779
  }
3688
3780
  async function mapConcurrent2(values, concurrency, map) {
3689
3781
  const results = new Array(values.length);
@@ -3881,7 +3973,7 @@ var ManifestBuilder = class {
3881
3973
  assets: uniqueAssets(assets)
3882
3974
  };
3883
3975
  }
3884
- build(entries, contentIndex) {
3976
+ build(entries, contentIndex, contentIndexAmbiguities = /* @__PURE__ */ new Map()) {
3885
3977
  const bySlug = new Map(entries.map((entry) => [entry.slug, entry]));
3886
3978
  const byContentId = createContentIdIndex(entries);
3887
3979
  const byAlias = createAliasIndex(entries);
@@ -3922,6 +4014,7 @@ var ManifestBuilder = class {
3922
4014
  outgoingLinks,
3923
4015
  incomingLinks,
3924
4016
  contentIndex,
4017
+ contentIndexAmbiguities,
3925
4018
  assets: [],
3926
4019
  clientEntries: [],
3927
4020
  diagnostics: [],
@@ -4038,10 +4131,10 @@ function buildOutputInventory(manifest, pluginPageOutputs = [], contentOutputDep
4038
4131
  for (const entry of manifest.publicEntries) {
4039
4132
  addOutput(outputs, contentOutput(entry, contentOutputDependencies));
4040
4133
  }
4041
- for (const [path5, redirect] of manifest.publicRedirects) {
4134
+ for (const [path6, redirect] of manifest.publicRedirects) {
4042
4135
  addOutput(outputs, {
4043
4136
  kind: "redirect",
4044
- path: htmlOutputPath(path5),
4137
+ path: htmlOutputPath(path6),
4045
4138
  producer: `content:${redirect.slug}:redirect`,
4046
4139
  dependencies: [{ type: "content", slug: redirect.slug }]
4047
4140
  });
@@ -4170,79 +4263,19 @@ function assertNoGeneratedOutputCollision(existing, incoming) {
4170
4263
  );
4171
4264
  }
4172
4265
  function normalizePublicPath(pathname) {
4173
- const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
4174
- return path5 === "/" ? path5 : path5.replace(/\/$/, "");
4266
+ const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
4267
+ return path6 === "/" ? path6 : path6.replace(/\/$/, "");
4175
4268
  }
4176
- function isMarkdownPath3(path5) {
4177
- return path5.endsWith(".md");
4269
+ function isMarkdownPath3(path6) {
4270
+ return path6.endsWith(".md");
4178
4271
  }
4179
- function toSlug3(path5) {
4180
- return path5.replace(/\.md$/, "");
4272
+ function toSlug3(path6) {
4273
+ return path6.replace(/\.md$/, "");
4181
4274
  }
4182
4275
  function compareOutput(left, right) {
4183
4276
  return left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind);
4184
4277
  }
4185
4278
 
4186
- // src/content/publishing.ts
4187
- function resolvePublishingState(frontmatter, options) {
4188
- const explicitVisibility = resolveExplicitVisibility(frontmatter?.visibility);
4189
- const publishAt = resolvePublishAt(frontmatter?.publishAt);
4190
- if (explicitVisibility === "draft")
4191
- return state("draft", false, false, publishAt);
4192
- if (publishAt && publishAt.getTime() > options.buildTime.getTime()) {
4193
- return state("scheduled", false, false, publishAt);
4194
- }
4195
- if (explicitVisibility === "unlisted") {
4196
- return state("unlisted", true, false, publishAt);
4197
- }
4198
- if (explicitVisibility === "public" || publishAt) {
4199
- return state("public", true, true, publishAt);
4200
- }
4201
- const routable = isPublishable(options.strategy, frontmatter);
4202
- return state(routable ? "public" : "draft", routable, routable, publishAt);
4203
- }
4204
- function resolvePublishingBuildTime(input) {
4205
- if (input === void 0) return /* @__PURE__ */ new Date();
4206
- if (input instanceof Date) {
4207
- if (Number.isNaN(input.getTime()))
4208
- throw new Error("Invalid publishing build time.");
4209
- return new Date(input.getTime());
4210
- }
4211
- const parsed = new Date(input);
4212
- if (Number.isNaN(parsed.getTime()))
4213
- throw new Error("Invalid publishing build time.");
4214
- return parsed;
4215
- }
4216
- function resolveExplicitVisibility(value) {
4217
- if (value === void 0) return void 0;
4218
- if (value === "public" || value === "unlisted" || value === "draft")
4219
- return value;
4220
- throw new Error(
4221
- `Invalid content visibility "${String(value)}". Expected public, unlisted, or draft.`
4222
- );
4223
- }
4224
- function resolvePublishAt(value) {
4225
- if (value === void 0) return void 0;
4226
- if (!(typeof value === "string" || value instanceof Date)) {
4227
- throw new Error("Invalid publishAt. Expected an ISO 8601 date string.");
4228
- }
4229
- const parsed = value instanceof Date ? value : new Date(value);
4230
- if (Number.isNaN(parsed.getTime())) {
4231
- throw new Error(
4232
- `Invalid publishAt "${String(value)}". Expected an ISO 8601 date string.`
4233
- );
4234
- }
4235
- return new Date(parsed.getTime());
4236
- }
4237
- function state(visibility, routable, discoverable, publishAt) {
4238
- return {
4239
- visibility,
4240
- routable,
4241
- discoverable,
4242
- ...publishAt ? { publishAt } : {}
4243
- };
4244
- }
4245
-
4246
4279
  // src/content/content_manager.ts
4247
4280
  var CONTENT_PROCESSING_CONCURRENCY = 16;
4248
4281
  var ContentManager = class {
@@ -4300,6 +4333,7 @@ var ContentManager = class {
4300
4333
  manifestBuilder = new ManifestBuilder();
4301
4334
  pluginRuntime;
4302
4335
  contentIndex = null;
4336
+ contentIndexAmbiguities = /* @__PURE__ */ new Map();
4303
4337
  contentIndexPromise = null;
4304
4338
  contentCache = /* @__PURE__ */ new Map();
4305
4339
  manifest = null;
@@ -4328,15 +4362,10 @@ var ContentManager = class {
4328
4362
  async getContentIndex(preparation) {
4329
4363
  if (this.contentIndex) return this.contentIndex;
4330
4364
  if (!preparation && this.contentIndexPromise)
4331
- return await this.contentIndexPromise;
4332
- if (preparation?.previousState && !hasContentChanges(preparation.changeSet)) {
4333
- this.contentIndex = new Map(
4334
- Object.entries(preparation.previousState.contentIndex)
4335
- );
4336
- return this.contentIndex;
4337
- }
4365
+ return (await this.contentIndexPromise).index;
4338
4366
  if (preparation) {
4339
4367
  this.contentIndex = preparation.currentContentIndex;
4368
+ this.contentIndexAmbiguities = preparation.currentContentIndexAmbiguities;
4340
4369
  return this.contentIndex;
4341
4370
  }
4342
4371
  const promise = this.observability().tracer.span(
@@ -4349,7 +4378,9 @@ var ContentManager = class {
4349
4378
  );
4350
4379
  if (!preparation) this.contentIndexPromise = promise;
4351
4380
  try {
4352
- this.contentIndex = await promise;
4381
+ const built = await promise;
4382
+ this.contentIndex = built.index;
4383
+ this.contentIndexAmbiguities = built.ambiguities;
4353
4384
  return this.contentIndex;
4354
4385
  } finally {
4355
4386
  if (!preparation) this.contentIndexPromise = null;
@@ -4364,48 +4395,55 @@ var ContentManager = class {
4364
4395
  contentPath: `${slug}.md`
4365
4396
  },
4366
4397
  async () => {
4367
- const [contentIndex, rawPost] = await Promise.all([
4368
- this.getContentIndex(),
4369
- this.getPost(slug)
4370
- ]);
4371
- await this.pluginRuntime.startBuild(contentIndex);
4372
- await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
4373
- await this.ensureRoutableSlugs();
4374
- this.pipeline ??= new Pipeline(
4375
- contentIndex,
4376
- await this.locationResolver.getPermalinks(),
4377
- (postSlug) => this.getPost(postSlug),
4378
- this.pipelineOptions,
4379
- this.isBuildTime
4380
- );
4381
- const content = await this.pipeline.execute(rawPost, {
4382
- sourceSlug: slug
4383
- });
4384
- this.processedContentCount += 1;
4385
- if (this.processedContentCount % 100 === 0) {
4386
- this.observability().tracer.event("content.process.sample", {
4387
- processed: this.processedContentCount
4388
- });
4398
+ try {
4399
+ return await this.processContent(slug);
4400
+ } catch (error) {
4401
+ throw attachErrorPath(error, `${slug}.md`);
4389
4402
  }
4390
- await this.pluginRuntime.runPostHook(
4391
- "onPostParsed",
4392
- slug,
4393
- rawPost,
4394
- content,
4395
- contentIndex
4396
- );
4397
- await this.pluginRuntime.runPostHook(
4398
- "onPostProcessed",
4399
- slug,
4400
- rawPost,
4401
- content,
4402
- contentIndex
4403
- );
4404
- this.contentCache.set(slug, content);
4405
- return content;
4406
4403
  }
4407
4404
  );
4408
4405
  }
4406
+ async processContent(slug) {
4407
+ const [contentIndex, rawPost] = await Promise.all([
4408
+ this.getContentIndex(),
4409
+ this.getPost(slug)
4410
+ ]);
4411
+ await this.pluginRuntime.startBuild(contentIndex);
4412
+ await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
4413
+ await this.ensureRoutableSlugs();
4414
+ this.pipeline ??= new Pipeline(
4415
+ contentIndex,
4416
+ await this.locationResolver.getPermalinks(),
4417
+ (postSlug) => this.getPost(postSlug),
4418
+ this.pipelineOptions,
4419
+ this.isBuildTime
4420
+ );
4421
+ const content = await this.pipeline.execute(rawPost, {
4422
+ sourceSlug: slug
4423
+ });
4424
+ this.processedContentCount += 1;
4425
+ if (this.processedContentCount % 100 === 0) {
4426
+ this.observability().tracer.event("content.process.sample", {
4427
+ processed: this.processedContentCount
4428
+ });
4429
+ }
4430
+ await this.pluginRuntime.runPostHook(
4431
+ "onPostParsed",
4432
+ slug,
4433
+ rawPost,
4434
+ content,
4435
+ contentIndex
4436
+ );
4437
+ await this.pluginRuntime.runPostHook(
4438
+ "onPostProcessed",
4439
+ slug,
4440
+ rawPost,
4441
+ content,
4442
+ contentIndex
4443
+ );
4444
+ this.contentCache.set(slug, content);
4445
+ return content;
4446
+ }
4409
4447
  async getManifest(options) {
4410
4448
  if (this.manifest) return this.manifest;
4411
4449
  if (this.manifestPromise) return await this.manifestPromise;
@@ -4471,7 +4509,11 @@ var ContentManager = class {
4471
4509
  const manifest = await this.observability().tracer.span(
4472
4510
  "content.graph",
4473
4511
  {},
4474
- () => this.manifestBuilder.build(entries, contentIndex)
4512
+ () => this.manifestBuilder.build(
4513
+ entries,
4514
+ contentIndex,
4515
+ this.contentIndexAmbiguities
4516
+ )
4475
4517
  );
4476
4518
  const cacheManifestEntries = structuredClone(manifest.entries);
4477
4519
  this.locationResolver.populateRedirects(manifest, locations);
@@ -4707,7 +4749,13 @@ var ContentManager = class {
4707
4749
  const routableSlugs = /* @__PURE__ */ new Set();
4708
4750
  await mapConcurrent3(posts, CONTENT_PROCESSING_CONCURRENCY, async (post) => {
4709
4751
  const markdown = await this.getPost(post.slug);
4710
- const publishing = resolvePublishingState(readFrontmatter(markdown), {
4752
+ let frontmatter;
4753
+ try {
4754
+ frontmatter = readFrontmatter(markdown);
4755
+ } catch (error) {
4756
+ throw attachErrorPath(error, `${post.slug}.md`);
4757
+ }
4758
+ const publishing = resolvePublishingState(frontmatter, {
4711
4759
  strategy,
4712
4760
  buildTime: this.publishingBuildTime
4713
4761
  });
@@ -4797,7 +4845,12 @@ function resolveFolderLocation(manifest, folder) {
4797
4845
  if (candidate) return { type: "content", entry: candidate.entry };
4798
4846
  }
4799
4847
  if (candidates.length > 1) return { type: "ambiguous", candidates };
4800
- const location = manifest.folderLocations.get(normalizeFolder2(folder));
4848
+ const normalizedFolder = normalizeFolder2(folder);
4849
+ const direct = manifest.folderLocations.get(normalizedFolder);
4850
+ if (direct) return { type: "generated", pathname: direct.pathname };
4851
+ const location = [...manifest.folderLocations.values()].find(
4852
+ (candidate) => candidate.folder === normalizedFolder
4853
+ );
4801
4854
  return location ? { type: "generated", pathname: location.pathname } : { type: "none" };
4802
4855
  }
4803
4856
  function resolvePublicFolderLocation(manifest, pathname) {
@@ -4811,12 +4864,15 @@ function resolvePublicFolderLocation(manifest, pathname) {
4811
4864
  );
4812
4865
  return location ? { type: "generated", pathname: location.pathname } : { type: "none" };
4813
4866
  }
4814
- function resolveGeneratedFolderLocation(manifest, folder) {
4867
+ function resolveGeneratedFolderLocation(manifest, folder, language) {
4815
4868
  const normalizedFolder = normalizeFolder2(folder);
4816
4869
  if (!normalizedFolder) return null;
4817
4870
  const candidates = /* @__PURE__ */ new Set();
4818
4871
  const prefix = `${normalizedFolder}/`;
4819
4872
  for (const entry of manifest.publicEntries) {
4873
+ if (language !== void 0 && entry.publicLocation.language !== language) {
4874
+ continue;
4875
+ }
4820
4876
  if (!entry.slug.startsWith(prefix)) continue;
4821
4877
  const remainder = entry.slug.slice(prefix.length).split("/").filter(Boolean);
4822
4878
  const pathname = parentPath(
@@ -5031,7 +5087,7 @@ function clampFinite(value, min, max) {
5031
5087
  // src/content/read_only_content_graph.ts
5032
5088
  async function readOnlyContentGraph(source, locations) {
5033
5089
  const entries = await source.scan();
5034
- const contentIndex = await new ContentIndexBuilder(source).build(entries);
5090
+ const { index: contentIndex, ambiguities: contentIndexAmbiguities } = await new ContentIndexBuilder(source).build(entries);
5035
5091
  const markdownEntries = entries.filter((entry) => entry.path.endsWith(".md")).toSorted((left, right) => left.path.localeCompare(right.path));
5036
5092
  const markdownContents = await Promise.all(
5037
5093
  markdownEntries.map(async (entry) => ({
@@ -5042,7 +5098,11 @@ async function readOnlyContentGraph(source, locations) {
5042
5098
  const graphEntries = markdownContents.map(
5043
5099
  ({ entry, markdown }) => createGraphEntry(entry, markdown, contentIndex, locations)
5044
5100
  );
5045
- return new ManifestBuilder().build(graphEntries, contentIndex).graph;
5101
+ return new ManifestBuilder().build(
5102
+ graphEntries,
5103
+ contentIndex,
5104
+ contentIndexAmbiguities
5105
+ ).graph;
5046
5106
  }
5047
5107
  function createGraphEntry(entry, markdown, contentIndex, locations) {
5048
5108
  const slug = entry.path.replace(/\.md$/, "");
@@ -5076,6 +5136,17 @@ async function readText3(source, entry) {
5076
5136
  return typeof content === "string" ? content : new TextDecoder().decode(content);
5077
5137
  }
5078
5138
 
5139
+ // src/project/project_layout.ts
5140
+ import path5 from "node:path";
5141
+ function resolveManagedStateRoot(config) {
5142
+ const directory = config?.buildDirectory;
5143
+ return directory ? path5.resolve(directory) : void 0;
5144
+ }
5145
+ function resolveBuildOutputDirectory(config) {
5146
+ const directory = config?.outputDirectory;
5147
+ return directory ? path5.resolve(directory) : void 0;
5148
+ }
5149
+
5079
5150
  // src/types/content_manifest.ts
5080
5151
  function appendContentBodySlot(entry, slot, html) {
5081
5152
  if (!html.trim()) return;
@@ -5088,12 +5159,12 @@ ${html}` : html
5088
5159
  }
5089
5160
 
5090
5161
  // src/types/plugin_endpoint.ts
5091
- function defineEndpoint(path5, handler, options = {}) {
5162
+ function defineEndpoint(path6, handler, options = {}) {
5092
5163
  if (options.cacheControl === void 0) {
5093
- return { path: path5, handler };
5164
+ return { path: path6, handler };
5094
5165
  }
5095
5166
  return {
5096
- path: path5,
5167
+ path: path6,
5097
5168
  handler: async (context) => {
5098
5169
  const response = await handler(context);
5099
5170
  const headers = { ...response.headers };
@@ -5142,6 +5213,7 @@ export {
5142
5213
  PluginDependencyError,
5143
5214
  SinkTracer,
5144
5215
  appendContentBodySlot,
5216
+ attachErrorPath,
5145
5217
  attachmentUrl,
5146
5218
  buildContentCollections,
5147
5219
  buildGraphEdges,
@@ -5193,6 +5265,7 @@ export {
5193
5265
  readContentBuildStateStatus,
5194
5266
  readContentSourceEntry,
5195
5267
  readOnlyContentGraph,
5268
+ resolveBuildOutputDirectory,
5196
5269
  resolveConfig,
5197
5270
  resolveConfigModule,
5198
5271
  resolveContentBuildStatePath,
@@ -5202,6 +5275,7 @@ export {
5202
5275
  resolveFolderEntry,
5203
5276
  resolveFolderLocation,
5204
5277
  resolveGeneratedFolderLocation,
5278
+ resolveManagedStateRoot,
5205
5279
  resolvePlugins,
5206
5280
  resolvePublicFolderLocation,
5207
5281
  resolvePublishingBuildTime,
@@ -4,5 +4,7 @@ import type { RiebeckiteConfig } from "./types/riebeckite_config.js";
4
4
  export declare function defineConfig(config: RiebeckiteConfig): RiebeckiteConfig;
5
5
  export declare function resolveConfigModule(configModule: unknown): ResolvedRiebeckiteConfig;
6
6
  export declare function resolveConfig(config: RiebeckiteConfig): ResolvedRiebeckiteConfig;
7
- export declare function isPublished(config: ResolvedRiebeckiteConfig, frontmatter: PostFrontmatter | undefined): boolean;
7
+ export declare function isPublished(config: ResolvedRiebeckiteConfig, frontmatter: PostFrontmatter | undefined, options?: {
8
+ buildTime?: Date | string;
9
+ }): boolean;
8
10
  export declare function isExcluded(patterns: readonly string[], relativePath: string): boolean;
@@ -12,6 +12,7 @@ export type ContentBuildPreparation = {
12
12
  readonly currentEntries: readonly FingerprintedContentEntry[];
13
13
  readonly currentContentAliases: ReadonlyMap<string, readonly string[]>;
14
14
  readonly currentContentIndex: Map<string, string>;
15
+ readonly currentContentIndexAmbiguities: ReadonlyMap<string, readonly string[]>;
15
16
  readonly changeSet: ContentChangeSet;
16
17
  readonly affectedContent: ReturnType<typeof determineAffectedContent>;
17
18
  };
@@ -1,7 +1,7 @@
1
1
  import type { ContentManifestEntry } from "../types/content_manifest.js";
2
2
  import type { ContentSourceEntry } from "./content_source.js";
3
3
  import type { OutputDescriptor } from "./output_dependency.js";
4
- export declare const CONTENT_BUILD_STATE_VERSION = 7;
4
+ export declare const CONTENT_BUILD_STATE_VERSION = 8;
5
5
  /**
6
6
  * Build state lives under `<content directory>/.riebeckite`. That directory is
7
7
  * build-time state, not content, so content sources must exclude it from scans
@@ -1,9 +1,13 @@
1
1
  import type { ContentSource, ContentSourceEntry } from "./content_source.js";
2
+ export type ContentIndexBuild = {
3
+ readonly index: Map<string, string>;
4
+ readonly ambiguities: ReadonlyMap<string, readonly string[]>;
5
+ };
2
6
  export declare class ContentIndexBuilder {
3
7
  private source;
4
8
  constructor(source: ContentSource);
5
- build(contentEntries: readonly ContentSourceEntry[], read?: (entry: ContentSourceEntry) => Promise<import("./content_source.js").ContentSourceContent>): Promise<Map<string, string>>;
6
- static buildFromAliases(contentEntries: readonly ContentSourceEntry[], aliasesByPath: ReadonlyMap<string, readonly string[]>): Map<string, string>;
9
+ build(contentEntries: readonly ContentSourceEntry[], read?: (entry: ContentSourceEntry) => Promise<import("./content_source.js").ContentSourceContent>): Promise<ContentIndexBuild>;
10
+ static buildFromAliases(contentEntries: readonly ContentSourceEntry[], aliasesByPath: ReadonlyMap<string, readonly string[]>): ContentIndexBuild;
7
11
  private indexContentEntry;
8
12
  private static indexContentEntryFromAliases;
9
13
  }
@@ -29,6 +29,7 @@ export declare class ContentManager {
29
29
  private manifestBuilder;
30
30
  private pluginRuntime;
31
31
  private contentIndex;
32
+ private contentIndexAmbiguities;
32
33
  private contentIndexPromise;
33
34
  private contentCache;
34
35
  private manifest;
@@ -51,6 +52,7 @@ export declare class ContentManager {
51
52
  getPost(slug: string): Promise<string>;
52
53
  getContentIndex(preparation?: ContentBuildPreparation): Promise<Map<string, string>>;
53
54
  getProcessedContent(slug: string): Promise<PostContent>;
55
+ private processContent;
54
56
  getManifest(options?: ContentBuildOptions): Promise<ContentManifest>;
55
57
  private buildManifest;
56
58
  build(options?: ContentBuildOptions): Promise<ContentManifest>;
@@ -33,4 +33,4 @@ export type FolderLocationResolution = {
33
33
  export declare function resolveFolderEntry(manifest: ContentManifest, folder: string, options?: ResolveFolderEntryOptions): FolderEntryResolution;
34
34
  export declare function resolveFolderLocation(manifest: ContentManifest, folder: string): FolderLocationResolution;
35
35
  export declare function resolvePublicFolderLocation(manifest: ContentManifest, pathname: string): FolderLocationResolution;
36
- export declare function resolveGeneratedFolderLocation(manifest: ContentManifest, folder: string): string | null;
36
+ export declare function resolveGeneratedFolderLocation(manifest: ContentManifest, folder: string, language?: string): string | null;
@@ -3,5 +3,5 @@ import type { PostContent } from "../types/post_content.js";
3
3
  import type { ResolvedPublishingState } from "./publishing.js";
4
4
  export declare class ManifestBuilder {
5
5
  createEntry(slug: string, markdown: string, processed: PostContent, contentIndex: Map<string, string>, location: ContentPublicLocation, publishing: ResolvedPublishingState): ContentManifestEntry;
6
- build(entries: ContentManifestEntry[], contentIndex: Map<string, string>): ContentManifest;
6
+ build(entries: ContentManifestEntry[], contentIndex: Map<string, string>, contentIndexAmbiguities?: ReadonlyMap<string, readonly string[]>): ContentManifest;
7
7
  }
@@ -2,6 +2,7 @@ import type { Tracer } from "../observability.js";
2
2
  import type { RiebeckitePlugin } from "../types/plugin.js";
3
3
  import type { PluginLifecycleContext, PluginManifestContext } from "../types/plugin_context.js";
4
4
  export declare class PluginHookError extends Error {
5
+ readonly path?: string;
5
6
  constructor(pluginName: string, hookName: string, cause: unknown);
6
7
  }
7
8
  export declare function runPluginHook<TContext extends {
@@ -0,0 +1,11 @@
1
+ import type { ResolvedRiebeckiteConfig } from "../types/resolved_riebeckite_config.js";
2
+ /**
3
+ * Root directory for Riebeckite-managed persistent state: content build state,
4
+ * plugin cache, persistent content cache, and the SSG output cache. The root is
5
+ * reproducible and safe to remove; missing paths are not an error.
6
+ */
7
+ export declare function resolveManagedStateRoot(config: ResolvedRiebeckiteConfig | undefined): string | undefined;
8
+ /**
9
+ * Directory for the built site. Missing paths are not an error.
10
+ */
11
+ export declare function resolveBuildOutputDirectory(config: ResolvedRiebeckiteConfig | undefined): string | undefined;
@@ -77,6 +77,8 @@ export type ContentPublicLocation = {
77
77
  };
78
78
  export type FolderLocation = {
79
79
  pathname: string;
80
+ folder?: string;
81
+ language?: string;
80
82
  };
81
83
  export type PluginPageRoute = {
82
84
  pathname: string;
@@ -121,6 +123,13 @@ export type ContentManifest = {
121
123
  outgoingLinks: Map<string, ContentLink[]>;
122
124
  incomingLinks: Map<string, string[]>;
123
125
  contentIndex: Map<string, string>;
126
+ /**
127
+ * Lookup keys that matched more than one candidate, mapped to their
128
+ * candidates (sorted). These keys are intentionally absent from
129
+ * `contentIndex` so links stay unresolved instead of picking a target by
130
+ * discovery order. Used to report actionable ambiguity diagnostics.
131
+ */
132
+ contentIndexAmbiguities?: ReadonlyMap<string, readonly string[]>;
124
133
  graph: ContentGraph;
125
134
  assets: ContentManifestPluginAsset[];
126
135
  /**
@@ -25,7 +25,7 @@ export type RenderableFeedEntry = ContentManifestEntry & {
25
25
  html?: string;
26
26
  };
27
27
  export type PluginSeoExtension = {
28
- buildArticleSeo(config: ResolvedRiebeckiteConfig, permalink: string, post: PostContent, headTags?: readonly PluginHeadTag[]): SeoMetadata;
28
+ buildArticleSeo(config: ResolvedRiebeckiteConfig, permalink: string, post: PostContent, headTags?: readonly PluginHeadTag[], language?: string): SeoMetadata;
29
29
  buildWebsiteSeo(config: ResolvedRiebeckiteConfig, input: WebsiteSeoInput, headTags?: readonly PluginHeadTag[]): SeoMetadata;
30
30
  buildAbsoluteUrl(config: ResolvedRiebeckiteConfig, pathOrUrl: string): string;
31
31
  buildPostUrl(config: ResolvedRiebeckiteConfig, permalink: string): string;
@@ -9,6 +9,11 @@ export type ResolvedRiebeckiteConfig = {
9
9
  * relative to their application root.
10
10
  */
11
11
  buildDirectory?: string;
12
+ /**
13
+ * Directory for the built site. Host integrations resolve this relative to
14
+ * their application root.
15
+ */
16
+ outputDirectory?: string;
12
17
  site: Omit<Required<SiteConfig>, "feed"> & {
13
18
  feed: {
14
19
  title: string;
@@ -0,0 +1 @@
1
+ export declare function attachErrorPath<T>(error: T, path: string): T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/core",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "tsx": "^4.22.4",
52
- "@riebeckite/test": "0.0.17"
52
+ "@riebeckite/test": "0.0.18"
53
53
  },
54
54
  "repository": {
55
55
  "type": "git",