@riebeckite/core 0.0.16 → 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/dist/index.js CHANGED
@@ -18,55 +18,12 @@ function validateConfigIssues(config) {
18
18
  return [{ path: "config", message: "Expected an object." }];
19
19
  }
20
20
  validateSite(value.site, issues);
21
- validateNavigation(value.navigation, issues);
22
21
  validateContent(value.content, issues);
22
+ validateCache(value.cache, issues);
23
23
  validateTheme(value.theme, issues);
24
24
  validatePlugins(value.plugins, issues);
25
25
  return issues;
26
26
  }
27
- function validateNavigation(value, issues) {
28
- if (value === void 0) return;
29
- if (!isRecord(value)) {
30
- issues.push({ path: "navigation", message: "Expected an object." });
31
- return;
32
- }
33
- validateNavigationItems(value.header, "navigation.header", issues, /* @__PURE__ */ new Set());
34
- validateNavigationItems(value.footer, "navigation.footer", issues, /* @__PURE__ */ new Set());
35
- }
36
- function validateNavigationItems(value, path5, issues, ancestors) {
37
- if (value === void 0) return;
38
- if (!Array.isArray(value)) {
39
- issues.push({ path: path5, message: "Expected an array." });
40
- return;
41
- }
42
- for (const [index, item] of value.entries()) {
43
- const itemPath = `${path5}[${index}]`;
44
- if (!isRecord(item)) {
45
- issues.push({ path: itemPath, message: "Expected an object." });
46
- continue;
47
- }
48
- if (ancestors.has(item)) {
49
- issues.push({
50
- path: itemPath,
51
- message: "Navigation children must not be recursive."
52
- });
53
- continue;
54
- }
55
- validateRequiredString(item.label, `${itemPath}.label`, issues);
56
- validateRequiredString(item.href, `${itemPath}.href`, issues);
57
- validateOptionalBoolean(item.external, `${itemPath}.external`, issues);
58
- if (item.children !== void 0) {
59
- const nextAncestors = new Set(ancestors);
60
- nextAncestors.add(item);
61
- validateNavigationItems(
62
- item.children,
63
- `${itemPath}.children`,
64
- issues,
65
- nextAncestors
66
- );
67
- }
68
- }
69
- }
70
27
  function validateSite(value, issues) {
71
28
  if (!isRecord(value)) {
72
29
  issues.push({ path: "site", message: "Expected an object." });
@@ -137,6 +94,15 @@ function validatePublishStrategy(value, issues) {
137
94
  });
138
95
  }
139
96
  }
97
+ function validateCache(value, issues) {
98
+ if (value === void 0) return;
99
+ if (!isRecord(value)) {
100
+ issues.push({ path: "cache", message: "Expected an object." });
101
+ return;
102
+ }
103
+ validateOptionalBoolean(value.enabled, "cache.enabled", issues);
104
+ validateOptionalString(value.directory, "cache.directory", issues);
105
+ }
140
106
  function validateTheme(value, issues) {
141
107
  if (value === void 0) return;
142
108
  if (!isRecord(value)) {
@@ -153,41 +119,41 @@ function validateTheme(value, issues) {
153
119
  }
154
120
  }
155
121
  }
156
- function validateThemeConfig(value, path5, issues) {
157
- validateOptionalString(value.name, `${path5}.name`, issues);
122
+ function validateThemeConfig(value, path6, issues) {
123
+ validateOptionalString(value.name, `${path6}.name`, issues);
158
124
  validateOneOf(
159
125
  value.colorMode,
160
- `${path5}.colorMode`,
126
+ `${path6}.colorMode`,
161
127
  ["light", "dark", "system"],
162
128
  issues
163
129
  );
164
130
  validateOneOf(
165
131
  value.typography,
166
- `${path5}.typography`,
132
+ `${path6}.typography`,
167
133
  ["system", "serif", "sans"],
168
134
  issues
169
135
  );
170
136
  validateOneOf(
171
137
  value.articleLayout,
172
- `${path5}.articleLayout`,
138
+ `${path6}.articleLayout`,
173
139
  ["article", "sidebar", "full-width"],
174
140
  issues
175
141
  );
176
- validateStringArray(value.userCss, `${path5}.userCss`, issues);
142
+ validateStringArray(value.userCss, `${path6}.userCss`, issues);
177
143
  if (value.attributes !== void 0 && !isRecord(value.attributes)) {
178
- issues.push({ path: `${path5}.attributes`, message: "Expected an object." });
144
+ issues.push({ path: `${path6}.attributes`, message: "Expected an object." });
179
145
  }
180
146
  }
181
- function validateThemeStyles(value, path5, issues) {
147
+ function validateThemeStyles(value, path6, issues) {
182
148
  if (value === void 0) return;
183
149
  if (!Array.isArray(value)) {
184
- issues.push({ path: path5, message: "Expected an array." });
150
+ issues.push({ path: path6, message: "Expected an array." });
185
151
  return;
186
152
  }
187
153
  for (const [index, style] of value.entries()) {
188
154
  if (!isRecord(style) || !isNonEmptyString(style.moduleSpecifier)) {
189
155
  issues.push({
190
- path: `${path5}[${index}].moduleSpecifier`,
156
+ path: `${path6}[${index}].moduleSpecifier`,
191
157
  message: "Expected a non-empty string."
192
158
  });
193
159
  }
@@ -207,69 +173,69 @@ function validatePlugins(value, issues) {
207
173
  continue;
208
174
  }
209
175
  const plugin = input;
210
- const path5 = `plugins[${index}]`;
176
+ const path6 = `plugins[${index}]`;
211
177
  if (!isNonEmptyString(plugin.name)) {
212
178
  issues.push({
213
- path: `${path5}.name`,
179
+ path: `${path6}.name`,
214
180
  message: "Expected a non-empty string."
215
181
  });
216
182
  } else if (plugin.enabled !== false) {
217
183
  const duplicateIndex = names.get(plugin.name);
218
184
  if (duplicateIndex !== void 0) {
219
185
  issues.push({
220
- path: `${path5}.name`,
186
+ path: `${path6}.name`,
221
187
  message: `Duplicate plugin name "${plugin.name}" (also plugins[${duplicateIndex}]).`
222
188
  });
223
189
  } else {
224
190
  names.set(plugin.name, index);
225
191
  }
226
192
  }
227
- validateOptionalBoolean(plugin.enabled, `${path5}.enabled`, issues);
228
- validateCapabilityMetadata(plugin, path5, issues);
229
- 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);
230
196
  }
231
197
  }
232
- function validateCapabilityMetadata(plugin, path5, issues) {
233
- validateStringArray(plugin.provides, `${path5}.provides`, issues, true);
234
- validateStringArray(plugin.requires, `${path5}.requires`, issues, true);
235
- 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);
236
202
  }
237
- function validatePluginOptions(plugin, path5, issues) {
203
+ function validatePluginOptions(plugin, path6, issues) {
238
204
  if (!plugin.validateOptions) return;
239
205
  try {
240
206
  const optionIssues = plugin.validateOptions(plugin.options) ?? [];
241
207
  for (const issue of optionIssues) {
242
208
  issues.push({
243
- path: issue.path ? `${path5}.options.${issue.path}` : `${path5}.options`,
209
+ path: issue.path ? `${path6}.options.${issue.path}` : `${path6}.options`,
244
210
  message: issue.message
245
211
  });
246
212
  }
247
213
  } catch (error) {
248
214
  issues.push({
249
- path: `${path5}.options`,
215
+ path: `${path6}.options`,
250
216
  message: `Validator failed: ${toErrorMessage(error)}`
251
217
  });
252
218
  }
253
219
  }
254
- function validateRequiredString(value, path5, issues) {
220
+ function validateRequiredString(value, path6, issues) {
255
221
  if (!isNonEmptyString(value)) {
256
- issues.push({ path: path5, message: "Expected a non-empty string." });
222
+ issues.push({ path: path6, message: "Expected a non-empty string." });
257
223
  }
258
224
  }
259
- function validateOptionalString(value, path5, issues) {
225
+ function validateOptionalString(value, path6, issues) {
260
226
  if (value !== void 0 && typeof value !== "string") {
261
- issues.push({ path: path5, message: "Expected a string." });
227
+ issues.push({ path: path6, message: "Expected a string." });
262
228
  }
263
229
  }
264
- function validateOptionalBoolean(value, path5, issues) {
230
+ function validateOptionalBoolean(value, path6, issues) {
265
231
  if (value !== void 0 && typeof value !== "boolean") {
266
- issues.push({ path: path5, message: "Expected a boolean." });
232
+ issues.push({ path: path6, message: "Expected a boolean." });
267
233
  }
268
234
  }
269
- function validateAbsoluteUrl(value, path5, issues) {
235
+ function validateAbsoluteUrl(value, path6, issues) {
270
236
  if (value === void 0 || value === "") return;
271
237
  if (typeof value !== "string") {
272
- issues.push({ path: path5, message: "Expected an absolute URL." });
238
+ issues.push({ path: path6, message: "Expected an absolute URL." });
273
239
  return;
274
240
  }
275
241
  try {
@@ -277,27 +243,27 @@ function validateAbsoluteUrl(value, path5, issues) {
277
243
  if (url.protocol !== "http:" && url.protocol !== "https:")
278
244
  throw new Error();
279
245
  } catch {
280
- issues.push({ path: path5, message: "Expected an absolute HTTP(S) URL." });
246
+ issues.push({ path: path6, message: "Expected an absolute HTTP(S) URL." });
281
247
  }
282
248
  }
283
- function validateStringArray(value, path5, issues, nonEmpty = false) {
249
+ function validateStringArray(value, path6, issues, nonEmpty = false) {
284
250
  if (value === void 0) return;
285
251
  if (!Array.isArray(value)) {
286
- issues.push({ path: path5, message: "Expected an array." });
252
+ issues.push({ path: path6, message: "Expected an array." });
287
253
  return;
288
254
  }
289
255
  for (const [index, item] of value.entries()) {
290
256
  if (typeof item !== "string" || nonEmpty && !item.trim()) {
291
257
  issues.push({
292
- path: `${path5}[${index}]`,
258
+ path: `${path6}[${index}]`,
293
259
  message: nonEmpty ? "Expected a non-empty string." : "Expected a string."
294
260
  });
295
261
  }
296
262
  }
297
263
  }
298
- function validateOneOf(value, path5, choices, issues) {
264
+ function validateOneOf(value, path6, choices, issues) {
299
265
  if (value !== void 0 && (typeof value !== "string" || !choices.includes(value))) {
300
- issues.push({ path: path5, message: `Expected one of: ${choices.join(", ")}.` });
266
+ issues.push({ path: path6, message: `Expected one of: ${choices.join(", ")}.` });
301
267
  }
302
268
  }
303
269
  function formatValidationIssues(issues) {
@@ -318,6 +284,74 @@ function toErrorMessage(error) {
318
284
  return error instanceof Error ? error.message : String(error);
319
285
  }
320
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
+
321
355
  // src/plugin/plugin_dependency_error.ts
322
356
  var PluginDependencyError = class extends Error {
323
357
  kind;
@@ -607,14 +641,6 @@ function getResolvedPluginMetadata(plugins) {
607
641
  }));
608
642
  }
609
643
 
610
- // src/types/publish_strategy.ts
611
- function isPublishable(strategy, frontmatter) {
612
- if (strategy === "explicit") {
613
- return frontmatter?.publish === true;
614
- }
615
- return !(frontmatter?.private === true || frontmatter?.draft === true);
616
- }
617
-
618
644
  // src/types/theme_config.ts
619
645
  function defineTheme(theme) {
620
646
  return theme;
@@ -669,23 +695,14 @@ function resolveConfig(config) {
669
695
  language: config.site?.feed?.language ?? config.site?.locale ?? "en"
670
696
  }
671
697
  },
672
- navigation: {
673
- header: config.navigation?.header ?? [],
674
- footer: config.navigation?.footer ?? []
675
- },
676
698
  content: {
677
- directory: config.content?.directory ?? "../../content",
699
+ directory: config.content?.directory ?? "content",
678
700
  source: config.content?.source,
679
701
  exclude: config.content?.exclude ?? [],
680
702
  filters: {
681
703
  publishStrategy: config.content?.filters?.publishStrategy ?? "explicit"
682
704
  }
683
705
  },
684
- markdown: {
685
- syntaxHighlight: {
686
- theme: config.markdown?.syntaxHighlight?.theme ?? ""
687
- }
688
- },
689
706
  theme,
690
707
  plugins: resolvePlugins(config.plugins),
691
708
  cache: {
@@ -723,10 +740,11 @@ function sanitizeThemeAttributes(attributes) {
723
740
  ).filter(([name]) => !reservedAttributes.has(name))
724
741
  );
725
742
  }
726
- function isPublished(config, frontmatter) {
727
- if (frontmatter?.visibility === "unlisted") return true;
728
- if (frontmatter?.visibility === "draft") return false;
729
- 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;
730
748
  }
731
749
  function isExcluded(patterns, relativePath) {
732
750
  const normalized = relativePath.replace(/\\/g, "/");
@@ -788,9 +806,9 @@ var configDeprecations = [];
788
806
  function collectConfigDeprecationDiagnostics(config, notices = configDeprecations) {
789
807
  return notices.filter((notice) => hasConfigPath(config, notice.path)).map(createDeprecationDiagnostic);
790
808
  }
791
- function hasConfigPath(config, path5) {
809
+ function hasConfigPath(config, path6) {
792
810
  let value = config;
793
- for (const segment of path5.split(".")) {
811
+ for (const segment of path6.split(".")) {
794
812
  if (!isRecord2(value) || !(segment in value)) return false;
795
813
  value = value[segment];
796
814
  }
@@ -853,7 +871,7 @@ import fs from "node:fs/promises";
853
871
  import path from "node:path";
854
872
 
855
873
  // src/content/content_build_state.ts
856
- var CONTENT_BUILD_STATE_VERSION = 6;
874
+ var CONTENT_BUILD_STATE_VERSION = 8;
857
875
  var CONTENT_BUILD_STATE_DIRECTORY = ".riebeckite";
858
876
  var CONTENT_BUILD_STATE_EXCLUDE = `${CONTENT_BUILD_STATE_DIRECTORY}/**`;
859
877
 
@@ -1258,9 +1276,9 @@ function buildContentCollections(entries, definitions) {
1258
1276
  function normalizePageSize(value) {
1259
1277
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1260
1278
  }
1261
- function withPage(path5, index) {
1262
- if (index === 0) return path5;
1263
- const base = path5 === "/" ? "" : path5;
1279
+ function withPage(path6, index) {
1280
+ if (index === 0) return path6;
1281
+ const base = path6 === "/" ? "" : path6;
1264
1282
  return normalizePath(`${base}/page/${index + 1}`);
1265
1283
  }
1266
1284
  function defaultPath(context) {
@@ -1370,6 +1388,23 @@ function isResolvedNoteLink(link) {
1370
1388
  return link.kind === "note" && link.slug !== null;
1371
1389
  }
1372
1390
 
1391
+ // src/content/content_language.ts
1392
+ function getEntryLanguage(entry) {
1393
+ return entry.publicLocation.language;
1394
+ }
1395
+ function selectEntriesByLanguage(entries, language) {
1396
+ if (!language) {
1397
+ return entries;
1398
+ }
1399
+ const localized = entries.filter(
1400
+ (entry) => getEntryLanguage(entry) !== void 0
1401
+ );
1402
+ if (localized.length === 0) {
1403
+ return entries;
1404
+ }
1405
+ return localized.filter((entry) => getEntryLanguage(entry) === language);
1406
+ }
1407
+
1373
1408
  // src/content/content_location.ts
1374
1409
  function resolveDefaultContentLocation(content) {
1375
1410
  return {
@@ -1527,6 +1562,8 @@ import { matter as matter2 } from "vfile-matter";
1527
1562
 
1528
1563
  // src/content/content_dependency_tracker.ts
1529
1564
  import { createHash as createHash2 } from "node:crypto";
1565
+ var LINK_INDEX_PREFIX = "index:";
1566
+ var LINK_LOCATION_PREFIX = "location:";
1530
1567
  function createContentDependencyTracker(source) {
1531
1568
  const dependencies = /* @__PURE__ */ new Map();
1532
1569
  const record = (identity, content) => {
@@ -1572,6 +1609,30 @@ function dependencyKey(dependency) {
1572
1609
  return `${normalized.kind}:${normalized.id}`;
1573
1610
  }
1574
1611
 
1612
+ // src/content/content_links.ts
1613
+ var WIKILINK_PATTERN = /(!)?\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
1614
+ function extractContentLinks(markdown, contentIndex) {
1615
+ WIKILINK_PATTERN.lastIndex = 0;
1616
+ const links = [];
1617
+ for (let match = WIKILINK_PATTERN.exec(markdown); match !== null; match = WIKILINK_PATTERN.exec(markdown)) {
1618
+ const rawTarget = match[2]?.trim();
1619
+ if (!rawTarget) continue;
1620
+ const resolved = contentIndex.get(rawTarget.toLowerCase()) ?? null;
1621
+ links.push({
1622
+ raw: rawTarget,
1623
+ slug: resolved,
1624
+ kind: resolved ? linkKind(resolved) : "unresolved",
1625
+ embed: match[1] === "!"
1626
+ });
1627
+ }
1628
+ return links;
1629
+ }
1630
+ function linkKind(value) {
1631
+ if (isImagePath(value)) return "image";
1632
+ if (isAttachmentPath(value)) return "attachment";
1633
+ return "note";
1634
+ }
1635
+
1575
1636
  // src/content/content_persistent_cache.ts
1576
1637
  import { createHash as createHash4 } from "node:crypto";
1577
1638
  import fs2 from "node:fs/promises";
@@ -1768,7 +1829,6 @@ function computePipelineFingerprint(config) {
1768
1829
  const fingerprintData = {
1769
1830
  compatibilityVersion: CONTENT_CACHE_COMPATIBILITY_VERSION,
1770
1831
  plugins: plugins.map((p) => pluginFingerprint(p)),
1771
- markdown: config.markdown,
1772
1832
  content: {
1773
1833
  filters: config.content.filters
1774
1834
  }
@@ -1933,6 +1993,7 @@ async function writeAtomically2(targetPath, versionDirectory2, serialized) {
1933
1993
  }
1934
1994
  function resolvePluginCacheDirectory(config) {
1935
1995
  if (!config) return path3.resolve(process.cwd(), ".riebeckite", "cache");
1996
+ if (config.cache?.directory) return config.cache.directory;
1936
1997
  if (config.buildDirectory) return path3.join(config.buildDirectory, "cache");
1937
1998
  return path3.resolve(config.content.directory, "..", ".riebeckite", "cache");
1938
1999
  }
@@ -2201,6 +2262,9 @@ var Pipeline = class {
2201
2262
  frontmatter: file.data.matter || {},
2202
2263
  html: String(file.value)
2203
2264
  };
2265
+ if (embedDepth === 0 && sourceSlug && dependencyTracker) {
2266
+ this.recordNoteLinkDependencies(markDownContent, dependencyTracker);
2267
+ }
2204
2268
  if (sourceSlug && dependencyTracker) {
2205
2269
  this.reportContentDependencies(
2206
2270
  sourceSlug,
@@ -2250,6 +2314,25 @@ var Pipeline = class {
2250
2314
  this.options.isRoutable
2251
2315
  );
2252
2316
  }
2317
+ recordNoteLinkDependencies(markdown, dependencyTracker) {
2318
+ for (const link of extractContentLinks(markdown, this.contentIndex)) {
2319
+ if (link.slug !== null && link.kind === "note") {
2320
+ const id = `${LINK_LOCATION_PREFIX}${link.slug}`;
2321
+ dependencyTracker.recordLinkResolution(
2322
+ id,
2323
+ this.resolveLinkDependency(id)
2324
+ );
2325
+ continue;
2326
+ }
2327
+ if (link.kind === "unresolved") {
2328
+ const id = `${LINK_INDEX_PREFIX}${link.raw.toLowerCase()}`;
2329
+ dependencyTracker.recordLinkResolution(
2330
+ id,
2331
+ this.resolveLinkDependency(id)
2332
+ );
2333
+ }
2334
+ }
2335
+ }
2253
2336
  use(processor, plugin, options) {
2254
2337
  const register = processor.use.bind(processor);
2255
2338
  if (options === void 0) {
@@ -2353,8 +2436,6 @@ function normalizeMarkdownLinks(context) {
2353
2436
  });
2354
2437
  };
2355
2438
  }
2356
- var LINK_INDEX_PREFIX = "index:";
2357
- var LINK_LOCATION_PREFIX = "location:";
2358
2439
  function resolveLinkDependencyValue(id, contentIndex, lookupPermalink, isRoutable) {
2359
2440
  if (id.startsWith(LINK_INDEX_PREFIX)) {
2360
2441
  return contentIndex.get(id.slice(LINK_INDEX_PREFIX.length)) ?? "";
@@ -2443,9 +2524,9 @@ function resolveRelativeMarkdownSlug(sourceSlug, targetPath) {
2443
2524
  }
2444
2525
  return normalizedParts.join("/").replace(/\.md$/i, "");
2445
2526
  }
2446
- function decodeUriPath(path5) {
2527
+ function decodeUriPath(path6) {
2447
2528
  try {
2448
- return decodeURI(path5);
2529
+ return decodeURI(path6);
2449
2530
  } catch {
2450
2531
  return null;
2451
2532
  }
@@ -2533,16 +2614,16 @@ var GeneratedOutputRegistry = class {
2533
2614
  outputs = [];
2534
2615
  owners = /* @__PURE__ */ new Map();
2535
2616
  sinkFor(pluginName) {
2536
- const register = (path5, output) => {
2537
- const owner = this.owners.get(path5);
2617
+ const register = (path6, output) => {
2618
+ const owner = this.owners.get(path6);
2538
2619
  if (owner) {
2539
2620
  throw new Error(
2540
- `Duplicate generated output path "${path5}" declared by "${owner}" and "${pluginName}".`
2621
+ `Duplicate generated output path "${path6}" declared by "${owner}" and "${pluginName}".`
2541
2622
  );
2542
2623
  }
2543
- this.owners.set(path5, pluginName);
2624
+ this.owners.set(path6, pluginName);
2544
2625
  this.outputs.push({
2545
- path: path5,
2626
+ path: path6,
2546
2627
  content: output.content,
2547
2628
  dependencies: output.dependencies,
2548
2629
  owner: pluginName
@@ -2565,12 +2646,29 @@ var GeneratedOutputRegistry = class {
2565
2646
  };
2566
2647
 
2567
2648
  // src/plugin/plugin_lifecycle.ts
2568
- var PluginLifecycleError = class extends Error {
2649
+ var PluginHookError = class extends Error {
2650
+ path;
2569
2651
  constructor(pluginName, hookName, cause) {
2570
2652
  super(`Plugin "${pluginName}" failed during "${hookName}"`, { cause });
2571
- this.name = "PluginLifecycleError";
2653
+ this.name = "PluginHookError";
2654
+ const path6 = cause?.path;
2655
+ if (typeof path6 === "string") this.path = path6;
2572
2656
  }
2573
2657
  };
2658
+ async function runPluginHook(plugin, hookName, context, hook) {
2659
+ try {
2660
+ return await context.tracer.span(
2661
+ `plugin.${hookName}`,
2662
+ {
2663
+ plugin: plugin.name,
2664
+ hook: hookName
2665
+ },
2666
+ () => hook(context)
2667
+ );
2668
+ } catch (error) {
2669
+ throw new PluginHookError(plugin.name, hookName, error);
2670
+ }
2671
+ }
2574
2672
  async function runSetup(plugins, createContext) {
2575
2673
  await runLifecycleHook(
2576
2674
  plugins,
@@ -2607,19 +2705,7 @@ async function runLifecycleHook(plugins, hookName, getHook, createContext) {
2607
2705
  for (const plugin of plugins) {
2608
2706
  const hook = getHook(plugin);
2609
2707
  if (!hook) continue;
2610
- try {
2611
- const context = createContext(plugin);
2612
- await context.tracer.span(
2613
- `plugin.${hookName}`,
2614
- {
2615
- plugin: plugin.name,
2616
- hook: hookName
2617
- },
2618
- () => hook(context)
2619
- );
2620
- } catch (error) {
2621
- throw new PluginLifecycleError(plugin.name, hookName, error);
2622
- }
2708
+ await runPluginHook(plugin, hookName, createContext(plugin), hook);
2623
2709
  }
2624
2710
  }
2625
2711
 
@@ -2637,6 +2723,7 @@ var PluginRuntime = class {
2637
2723
  generatedOutputs = new GeneratedOutputRegistry();
2638
2724
  isBuildTime = false;
2639
2725
  resolvedPlugins = null;
2726
+ pageDeclarations = /* @__PURE__ */ new WeakMap();
2640
2727
  getDiagnostics() {
2641
2728
  return this.diagnostics;
2642
2729
  }
@@ -2647,7 +2734,7 @@ var PluginRuntime = class {
2647
2734
  * Starts the plugin build lifecycle exactly once, regardless of which public
2648
2735
  * entry point (content processing or location resolution) reaches it first.
2649
2736
  * Concurrent callers await the same run, so the ordered hooks
2650
- * (`setup` → `buildStart` → `onBuildStart` → `onConfigResolved`) are never
2737
+ * (`setup` → `buildStart` → `onConfigResolved`) are never
2651
2738
  * duplicated and no caller proceeds before the lifecycle completes.
2652
2739
  */
2653
2740
  startBuild(contentIndex) {
@@ -2655,7 +2742,7 @@ var PluginRuntime = class {
2655
2742
  return this.buildLifecycle;
2656
2743
  }
2657
2744
  async runContentLoaded(slug, markdown, contentIndex) {
2658
- await this.runHook((plugin) => plugin.onContentLoaded, {
2745
+ await this.runHook("onContentLoaded", (plugin) => plugin.onContentLoaded, {
2659
2746
  ...this.createContext(contentIndex),
2660
2747
  slug,
2661
2748
  markdown
@@ -2667,22 +2754,30 @@ var PluginRuntime = class {
2667
2754
  for (const plugin of this.plugins()) {
2668
2755
  const resolver = plugin.resolveContentLocations;
2669
2756
  if (!resolver) continue;
2670
- const resolved = await resolver(
2671
- this.createPluginContext(plugin, context)
2757
+ const pluginContext = this.createPluginContext(plugin, context);
2758
+ const resolved = await runPluginHook(
2759
+ plugin,
2760
+ "resolveContentLocations",
2761
+ pluginContext,
2762
+ resolver
2672
2763
  );
2673
2764
  locations.push(...resolved);
2674
2765
  }
2675
2766
  return locations;
2676
2767
  }
2677
2768
  async extendContentLocations(entries, locations, contentIndex) {
2678
- await this.runHook((plugin) => plugin.extendContentLocations, {
2679
- ...this.createContext(contentIndex),
2680
- entries,
2681
- locations
2682
- });
2769
+ await this.runHook(
2770
+ "extendContentLocations",
2771
+ (plugin) => plugin.extendContentLocations,
2772
+ {
2773
+ ...this.createContext(contentIndex),
2774
+ entries,
2775
+ locations
2776
+ }
2777
+ );
2683
2778
  }
2684
2779
  async runPostHook(hookName, slug, markdown, content, contentIndex) {
2685
- await this.runHook((plugin) => plugin[hookName], {
2780
+ await this.runHook(hookName, (plugin) => plugin[hookName], {
2686
2781
  ...this.createContext(contentIndex),
2687
2782
  slug,
2688
2783
  markdown,
@@ -2690,16 +2785,24 @@ var PluginRuntime = class {
2690
2785
  });
2691
2786
  }
2692
2787
  async runGraphHook(entries, contentIndex) {
2693
- await this.runHook((plugin) => plugin.extendContentGraph, {
2694
- ...this.createContext(contentIndex),
2695
- entries
2696
- });
2788
+ await this.runHook(
2789
+ "extendContentGraph",
2790
+ (plugin) => plugin.extendContentGraph,
2791
+ {
2792
+ ...this.createContext(contentIndex),
2793
+ entries
2794
+ }
2795
+ );
2697
2796
  }
2698
2797
  async runManifestCreated(manifest, contentIndex) {
2699
- await this.runHook((plugin) => plugin.onManifestCreated, {
2700
- ...this.createContext(contentIndex),
2701
- manifest
2702
- });
2798
+ await this.runHook(
2799
+ "onManifestCreated",
2800
+ (plugin) => plugin.onManifestCreated,
2801
+ {
2802
+ ...this.createContext(contentIndex),
2803
+ manifest
2804
+ }
2805
+ );
2703
2806
  }
2704
2807
  async runBuildEnd(manifest, contentIndex) {
2705
2808
  const context = {
@@ -2710,10 +2813,6 @@ var PluginRuntime = class {
2710
2813
  this.plugins(),
2711
2814
  (plugin) => this.createPluginContext(plugin, context)
2712
2815
  );
2713
- await this.runHook((plugin) => plugin.onBuildEnd, {
2714
- ...this.createContext(contentIndex),
2715
- manifest
2716
- });
2717
2816
  }
2718
2817
  enableBuildTime() {
2719
2818
  this.isBuildTime = true;
@@ -2795,63 +2894,74 @@ var PluginRuntime = class {
2795
2894
  return highest[0].page;
2796
2895
  }
2797
2896
  async getPagePaths(manifest, contentIndex) {
2798
- const paths = [];
2799
- const context = { ...this.createContext(contentIndex), manifest };
2800
- for (const plugin of this.plugins()) {
2801
- for (const pageType of plugin.pageTypes ?? []) {
2802
- const declared = pageType.paths;
2803
- if (!declared) continue;
2804
- const resolved = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2805
- for (const rawPath of resolved) {
2806
- paths.push(normalizePageTypePath(rawPath, pageType));
2807
- }
2808
- }
2809
- }
2810
- return [...new Set(paths)];
2897
+ return [
2898
+ ...new Set(
2899
+ (await this.getPageDeclarations(manifest, contentIndex)).map(
2900
+ (declaration) => declaration.pathname
2901
+ )
2902
+ )
2903
+ ];
2811
2904
  }
2812
2905
  async getPageRoutes(manifest, contentIndex) {
2813
- const routes = [];
2814
- const context = { ...this.createContext(contentIndex), manifest };
2815
- for (const plugin of this.plugins()) {
2816
- for (const pageType of plugin.pageTypes ?? []) {
2817
- const declared = pageType.paths;
2818
- if (!declared) continue;
2819
- const paths = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2820
- for (const pathname of paths) {
2821
- routes.push({
2822
- pathname: normalizePageTypePath(pathname, pageType),
2823
- pluginName: plugin.name,
2824
- pageType: pageType.id
2825
- });
2826
- }
2827
- }
2828
- }
2829
- return routes;
2906
+ return (await this.getPageDeclarations(manifest, contentIndex)).map(
2907
+ ({ pathname, plugin, pageType }) => ({
2908
+ pathname,
2909
+ pluginName: plugin.name,
2910
+ pageType: pageType.id
2911
+ })
2912
+ );
2830
2913
  }
2831
2914
  async getPageOutputs(manifest, contentIndex) {
2832
- const outputs = [];
2915
+ const outputs = await Promise.all(
2916
+ (await this.getPageDeclarations(manifest, contentIndex)).map(
2917
+ async ({ pathname, plugin, pageType }) => {
2918
+ const context = {
2919
+ ...this.createContext(contentIndex),
2920
+ manifest,
2921
+ pathname
2922
+ };
2923
+ const dependencies = pageType.outputDependencies ? typeof pageType.outputDependencies === "function" ? await pageType.outputDependencies(
2924
+ this.createPluginContext(plugin, context)
2925
+ ) : pageType.outputDependencies : [{ type: "unknown" }];
2926
+ return {
2927
+ output: {
2928
+ kind: "plugin-page",
2929
+ path: pathname,
2930
+ producer: `plugin:${plugin.name}:page:${pageType.id}`,
2931
+ dependencies
2932
+ },
2933
+ priority: pageType.priority ?? 0
2934
+ };
2935
+ }
2936
+ )
2937
+ );
2938
+ return uniqueOutputs(outputs);
2939
+ }
2940
+ async getPageDeclarations(manifest, contentIndex) {
2941
+ const existing = this.pageDeclarations.get(manifest);
2942
+ if (existing) return await existing;
2943
+ const declarations = this.resolvePageDeclarations(manifest, contentIndex);
2944
+ this.pageDeclarations.set(manifest, declarations);
2945
+ return await declarations;
2946
+ }
2947
+ async resolvePageDeclarations(manifest, contentIndex) {
2948
+ const declarations = [];
2833
2949
  const context = { ...this.createContext(contentIndex), manifest };
2834
2950
  for (const plugin of this.plugins()) {
2835
2951
  for (const pageType of plugin.pageTypes ?? []) {
2836
- const declared = pageType.paths;
2837
- if (!declared) continue;
2838
- const paths = typeof declared === "function" ? await declared(this.createPluginContext(plugin, context)) : declared;
2839
- for (const rawPath of paths) {
2840
- const pathname = normalizePageTypePath(rawPath, pageType);
2841
- const dependencyContext = { ...context, pathname };
2842
- const dependencies = pageType.outputDependencies ? typeof pageType.outputDependencies === "function" ? await pageType.outputDependencies(
2843
- this.createPluginContext(plugin, dependencyContext)
2844
- ) : pageType.outputDependencies : [{ type: "unknown" }];
2845
- outputs.push({
2846
- kind: "plugin-page",
2847
- path: pathname,
2848
- producer: `plugin:${plugin.name}:page:${pageType.id}`,
2849
- dependencies
2952
+ const paths = pageType.paths;
2953
+ if (!paths) continue;
2954
+ const resolved = typeof paths === "function" ? await paths(this.createPluginContext(plugin, context)) : paths;
2955
+ for (const pathname of resolved) {
2956
+ declarations.push({
2957
+ pathname: normalizePageTypePath(pathname, pageType),
2958
+ plugin,
2959
+ pageType
2850
2960
  });
2851
2961
  }
2852
2962
  }
2853
2963
  }
2854
- return uniqueOutputs(outputs);
2964
+ return declarations;
2855
2965
  }
2856
2966
  getContentOutputDependencies() {
2857
2967
  return this.plugins().flatMap((plugin) => plugin.outputDependencies ?? []);
@@ -2862,9 +2972,13 @@ var PluginRuntime = class {
2862
2972
  const results = [];
2863
2973
  const context = this.createContext(contentIndex);
2864
2974
  for (const plugin of this.plugins()) {
2865
- const diagnostics = await plugin.addDiagnostics?.(
2866
- this.createPluginContext(plugin, context)
2867
- );
2975
+ const hook = plugin.addDiagnostics;
2976
+ const diagnostics = hook ? await runPluginHook(
2977
+ plugin,
2978
+ "addDiagnostics",
2979
+ this.createPluginContext(plugin, context),
2980
+ hook
2981
+ ) : void 0;
2868
2982
  for (const diagnostic of diagnostics ?? []) {
2869
2983
  results.push({
2870
2984
  ...diagnostic,
@@ -2893,13 +3007,13 @@ var PluginRuntime = class {
2893
3007
  stage: "buildStart",
2894
3008
  run: () => runBuildStart(this.plugins(), createPluginContext)
2895
3009
  },
2896
- {
2897
- stage: "onBuildStart",
2898
- run: () => this.runHook((plugin) => plugin.onBuildStart, context)
2899
- },
2900
3010
  {
2901
3011
  stage: "onConfigResolved",
2902
- run: () => this.runHook((plugin) => plugin.onConfigResolved, context)
3012
+ run: () => this.runHook(
3013
+ "onConfigResolved",
3014
+ (plugin) => plugin.onConfigResolved,
3015
+ context
3016
+ )
2903
3017
  }
2904
3018
  ];
2905
3019
  for (const { stage, run } of stages) {
@@ -2944,18 +3058,12 @@ var PluginRuntime = class {
2944
3058
  this.pluginCaches.set(plugin.name, cache);
2945
3059
  return cache;
2946
3060
  }
2947
- async runHook(hook, context) {
3061
+ async runHook(hookName, hook, context) {
2948
3062
  for (const plugin of this.plugins()) {
2949
3063
  const pluginHook = hook(plugin);
2950
3064
  if (!pluginHook) continue;
2951
3065
  const pluginContext = this.createPluginContext(plugin, context);
2952
- await pluginContext.tracer.span(
2953
- "plugin.hook",
2954
- {
2955
- plugin: plugin.name
2956
- },
2957
- () => pluginHook(pluginContext)
2958
- );
3066
+ await runPluginHook(plugin, hookName, pluginContext, pluginHook);
2959
3067
  }
2960
3068
  }
2961
3069
  observability() {
@@ -3029,22 +3137,40 @@ function normalizePageTypePath(pathname, pageType) {
3029
3137
  return pageType.directoryIndex ? normalizeDirectoryPagePath(pathname) : normalizePagePath(pathname);
3030
3138
  }
3031
3139
  function normalizeDirectoryPagePath(pathname) {
3032
- const path5 = normalizePagePath(pathname);
3033
- return path5 === "/" ? path5 : `${path5}/`;
3140
+ const path6 = normalizePagePath(pathname);
3141
+ return path6 === "/" ? path6 : `${path6}/`;
3034
3142
  }
3035
3143
  function normalizePagePath(pathname) {
3036
- const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3037
- return path5 === "/" ? path5 : path5.replace(/\/$/, "");
3144
+ const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
3145
+ return path6 === "/" ? path6 : path6.replace(/\/$/, "");
3038
3146
  }
3039
3147
  function countSeverity(diagnostics, severity) {
3040
3148
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
3041
3149
  }
3042
3150
  function uniqueOutputs(outputs) {
3043
3151
  const byPath = /* @__PURE__ */ new Map();
3044
- for (const output of outputs) byPath.set(output.path, output);
3045
- return [...byPath.values()].sort(
3046
- (left, right) => left.path.localeCompare(right.path)
3047
- );
3152
+ for (const output of outputs) {
3153
+ const previous = byPath.get(output.output.path);
3154
+ if (!previous || output.priority > previous.priority) {
3155
+ byPath.set(output.output.path, output);
3156
+ continue;
3157
+ }
3158
+ if (output.output.producer === previous.output.producer) continue;
3159
+ if (output.priority === previous.priority) {
3160
+ throw new Error(
3161
+ `Multiple plugin page types declare ${output.output.path} at priority ${output.priority}: ${previous.output.producer}, ${output.output.producer}`
3162
+ );
3163
+ }
3164
+ }
3165
+ return [...byPath.values()].map(({ output }) => output).sort((left, right) => left.path.localeCompare(right.path));
3166
+ }
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;
3048
3174
  }
3049
3175
 
3050
3176
  // src/content/affected_content.ts
@@ -3067,9 +3193,9 @@ function determineAffectedContent(changeSet, previousState, currentPaths, curren
3067
3193
  previousState.contentIndex,
3068
3194
  currentContentIndex
3069
3195
  );
3070
- for (const [path5, entry] of Object.entries(previousState.entries)) {
3071
- if (!isMarkdownPath2(path5)) continue;
3072
- const slug = toDependencyId(path5);
3196
+ for (const [path6, entry] of Object.entries(previousState.entries)) {
3197
+ if (!isMarkdownPath2(path6)) continue;
3198
+ const slug = toDependencyId(path6);
3073
3199
  if (direct.has(slug) || dependent.has(slug)) continue;
3074
3200
  if (!entry.linkTargets?.some((target) => changedIndexKeys.has(target))) {
3075
3201
  continue;
@@ -3106,27 +3232,27 @@ function findChangedIndexKeys(previousIndex, currentIndex) {
3106
3232
  return changed;
3107
3233
  }
3108
3234
  function addAllNotes(target, currentPaths, exclude) {
3109
- for (const path5 of currentPaths) {
3110
- if (!isMarkdownPath2(path5)) continue;
3111
- const slug = toDependencyId(path5);
3235
+ for (const path6 of currentPaths) {
3236
+ if (!isMarkdownPath2(path6)) continue;
3237
+ const slug = toDependencyId(path6);
3112
3238
  if (exclude?.has(slug)) continue;
3113
3239
  target.add(slug);
3114
3240
  }
3115
3241
  }
3116
- function isMarkdownPath2(path5) {
3117
- return path5.endsWith(".md");
3242
+ function isMarkdownPath2(path6) {
3243
+ return path6.endsWith(".md");
3118
3244
  }
3119
- function toDependencyId(path5) {
3120
- return path5.replace(/\.md$/, "");
3245
+ function toDependencyId(path6) {
3246
+ return path6.replace(/\.md$/, "");
3121
3247
  }
3122
- function toDependency(path5) {
3123
- 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, "/") };
3124
3250
  }
3125
3251
  function buildDependentsByDependency(state2) {
3126
3252
  const dependents = /* @__PURE__ */ new Map();
3127
- for (const [path5, entry] of Object.entries(state2.entries)) {
3128
- if (!isMarkdownPath2(path5)) continue;
3129
- const slug = toDependencyId(path5);
3253
+ for (const [path6, entry] of Object.entries(state2.entries)) {
3254
+ if (!isMarkdownPath2(path6)) continue;
3255
+ const slug = toDependencyId(path6);
3130
3256
  for (const dependency of entry.dependencies) {
3131
3257
  const key = dependencyKey2(dependency);
3132
3258
  const values = dependents.get(key) ?? /* @__PURE__ */ new Set();
@@ -3148,18 +3274,18 @@ function diffContentEntries(previous, current) {
3148
3274
  const added = [];
3149
3275
  const changed = [];
3150
3276
  const unchanged = [];
3151
- for (const [path5, fingerprint] of currentFingerprints) {
3152
- const previousEntry = previous.entries[path5];
3277
+ for (const [path6, fingerprint] of currentFingerprints) {
3278
+ const previousEntry = previous.entries[path6];
3153
3279
  if (!previousEntry) {
3154
- added.push(path5);
3280
+ added.push(path6);
3155
3281
  } else if (previousEntry.fingerprint !== fingerprint) {
3156
- changed.push(path5);
3282
+ changed.push(path6);
3157
3283
  } else {
3158
- unchanged.push(path5);
3284
+ unchanged.push(path6);
3159
3285
  }
3160
3286
  }
3161
3287
  const removed = Object.keys(previous.entries).filter(
3162
- (path5) => !currentFingerprints.has(path5)
3288
+ (path6) => !currentFingerprints.has(path6)
3163
3289
  );
3164
3290
  return sortChangeSet({ added, changed, removed, unchanged });
3165
3291
  }
@@ -3171,9 +3297,6 @@ function allContentChanged(entries) {
3171
3297
  unchanged: []
3172
3298
  });
3173
3299
  }
3174
- function hasContentChanges(changeSet) {
3175
- return changeSet.added.length > 0 || changeSet.changed.length > 0 || changeSet.removed.length > 0;
3176
- }
3177
3300
  function sortChangeSet(changeSet) {
3178
3301
  return {
3179
3302
  added: [...changeSet.added].sort(),
@@ -3249,54 +3372,73 @@ var ContentIndexBuilder = class _ContentIndexBuilder {
3249
3372
  }
3250
3373
  source;
3251
3374
  async build(contentEntries, read = (entry) => this.source.read(entry)) {
3252
- const index = /* @__PURE__ */ new Map();
3375
+ const candidates = /* @__PURE__ */ new Map();
3253
3376
  for (const contentEntry of contentEntries) {
3254
- await this.indexContentEntry(index, contentEntry, read);
3377
+ await this.indexContentEntry(candidates, contentEntry, read);
3255
3378
  }
3256
- return index;
3379
+ return resolveContentIndex(candidates);
3257
3380
  }
3258
3381
  static buildFromAliases(contentEntries, aliasesByPath) {
3259
- const index = /* @__PURE__ */ new Map();
3382
+ const candidates = /* @__PURE__ */ new Map();
3260
3383
  for (const contentEntry of contentEntries) {
3261
3384
  _ContentIndexBuilder.indexContentEntryFromAliases(
3262
- index,
3385
+ candidates,
3263
3386
  contentEntry,
3264
3387
  aliasesByPath.get(contentEntry.path) ?? []
3265
3388
  );
3266
3389
  }
3267
- return index;
3390
+ return resolveContentIndex(candidates);
3268
3391
  }
3269
- async indexContentEntry(index, contentEntry, read) {
3392
+ async indexContentEntry(candidates, contentEntry, read) {
3270
3393
  const aliases = contentEntry.path.toLowerCase().endsWith(".md") ? extractFrontmatterAliases(readText(await read(contentEntry))) : [];
3271
3394
  _ContentIndexBuilder.indexContentEntryFromAliases(
3272
- index,
3395
+ candidates,
3273
3396
  contentEntry,
3274
3397
  aliases
3275
3398
  );
3276
3399
  }
3277
- static indexContentEntryFromAliases(index, contentEntry, aliases) {
3400
+ static indexContentEntryFromAliases(candidates, contentEntry, aliases) {
3278
3401
  const contentPath = contentEntry.path;
3279
3402
  const ext = contentPath.split(".").pop()?.toLowerCase() ?? "";
3280
3403
  const value = ext === "md" ? contentPath.replace(/\.md$/, "") : contentPath;
3281
3404
  const parts = value.split("/");
3282
3405
  for (let i = parts.length - 1; i >= 0; i--) {
3283
3406
  const rawSuffix = parts.slice(i).join("/");
3284
- addIndexEntry(index, rawSuffix, value);
3407
+ addCandidate(candidates, rawSuffix, value);
3285
3408
  if (ext !== "md") {
3286
- addIndexEntry(index, rawSuffix.replace(/\.[^/.]+$/, ""), value);
3409
+ addCandidate(candidates, rawSuffix.replace(/\.[^/.]+$/, ""), value);
3287
3410
  }
3288
3411
  }
3289
3412
  if (ext === "md") {
3290
- for (const alias of aliases) addIndexEntry(index, alias, value);
3413
+ for (const alias of aliases) addCandidate(candidates, alias, value);
3291
3414
  }
3292
3415
  }
3293
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
+ }
3294
3434
  function readText(content) {
3295
3435
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3296
3436
  }
3297
- function addIndexEntry(index, key, value) {
3437
+ function addCandidate(candidates, key, value) {
3298
3438
  const normalizedKey = key.toLowerCase();
3299
- 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);
3300
3442
  }
3301
3443
 
3302
3444
  // src/content/content_build_coordinator.ts
@@ -3379,7 +3521,10 @@ var ContentBuildCoordinator = class {
3379
3521
  previousState,
3380
3522
  changeSet
3381
3523
  );
3382
- const currentContentIndex = await this.dependencies.observability.tracer.span(
3524
+ const {
3525
+ index: currentContentIndex,
3526
+ ambiguities: currentContentIndexAmbiguities
3527
+ } = await this.dependencies.observability.tracer.span(
3383
3528
  "content.index",
3384
3529
  {},
3385
3530
  () => ContentIndexBuilder.buildFromAliases(
@@ -3417,6 +3562,7 @@ var ContentBuildCoordinator = class {
3417
3562
  currentEntries,
3418
3563
  currentContentAliases,
3419
3564
  currentContentIndex,
3565
+ currentContentIndexAmbiguities,
3420
3566
  changeSet,
3421
3567
  affectedContent: affected
3422
3568
  };
@@ -3444,8 +3590,8 @@ var ContentBuildCoordinator = class {
3444
3590
  function readText2(content) {
3445
3591
  return typeof content === "string" ? content : new TextDecoder().decode(content);
3446
3592
  }
3447
- function toSlug(path5) {
3448
- return path5.replace(/\.md$/, "");
3593
+ function toSlug(path6) {
3594
+ return path6.replace(/\.md$/, "");
3449
3595
  }
3450
3596
  function collectDependencies2(manifestEntry, trackedDependencies) {
3451
3597
  if (!manifestEntry) return [];
@@ -3459,8 +3605,8 @@ function collectDependencies2(manifestEntry, trackedDependencies) {
3459
3605
  kind: "file",
3460
3606
  id: asset.path.replace(/\\/g, "/")
3461
3607
  })),
3462
- ...trackedDependencies.filter(
3463
- (dependency) => dependency.kind === "content" || dependency.kind === "file"
3608
+ ...trackedDependencies.map(toBuildDependency).filter(
3609
+ (dependency) => dependency !== null
3464
3610
  ).map((dependency) => ({
3465
3611
  kind: dependency.kind,
3466
3612
  id: dependency.id.replace(/\\/g, "/")
@@ -3474,6 +3620,18 @@ function collectDependencies2(manifestEntry, trackedDependencies) {
3474
3620
  (left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`)
3475
3621
  );
3476
3622
  }
3623
+ function toBuildDependency(dependency) {
3624
+ if (dependency.kind === "content" || dependency.kind === "file") {
3625
+ return { kind: dependency.kind, id: dependency.id };
3626
+ }
3627
+ if (dependency.id.startsWith(LINK_LOCATION_PREFIX)) {
3628
+ return {
3629
+ kind: "content",
3630
+ id: dependency.id.slice(LINK_LOCATION_PREFIX.length)
3631
+ };
3632
+ }
3633
+ return null;
3634
+ }
3477
3635
  function collectLinkTargets(manifestEntry) {
3478
3636
  if (!manifestEntry) return [];
3479
3637
  return [
@@ -3616,8 +3774,8 @@ var ContentLocationResolver = class {
3616
3774
  }
3617
3775
  }
3618
3776
  };
3619
- function toSlug2(path5) {
3620
- return path5.replace(/\.md$/, "");
3777
+ function toSlug2(path6) {
3778
+ return path6.replace(/\.md$/, "");
3621
3779
  }
3622
3780
  async function mapConcurrent2(values, concurrency, map) {
3623
3781
  const results = new Array(values.length);
@@ -3663,8 +3821,17 @@ var FileSystemContentSource = class {
3663
3821
  contentDirectory;
3664
3822
  exclude;
3665
3823
  async scan() {
3824
+ return (await this.scanWithExclusions()).entries;
3825
+ }
3826
+ async scanWithExclusions() {
3827
+ const exclusions = [];
3666
3828
  try {
3667
- return await this.scanDirectory(this.contentDirectory, "");
3829
+ const entries = await this.scanDirectory(
3830
+ this.contentDirectory,
3831
+ "",
3832
+ exclusions
3833
+ );
3834
+ return { entries, exclusions };
3668
3835
  } catch (error) {
3669
3836
  if (isMissingDirectoryError(error, this.contentDirectory)) {
3670
3837
  throw missingContentDirectoryError(this.contentDirectory);
@@ -3680,7 +3847,7 @@ var FileSystemContentSource = class {
3680
3847
  }
3681
3848
  return await fs4.readFile(filePath);
3682
3849
  }
3683
- async scanDirectory(directory, parentPath2) {
3850
+ async scanDirectory(directory, parentPath2, exclusions) {
3684
3851
  const directoryEntries = await fs4.readdir(directory, {
3685
3852
  withFileTypes: true
3686
3853
  });
@@ -3691,9 +3858,16 @@ var FileSystemContentSource = class {
3691
3858
  parentPath2 ? `${parentPath2}/${directoryEntry.name}` : directoryEntry.name
3692
3859
  );
3693
3860
  const filePath = path4.join(directory, directoryEntry.name);
3694
- if (this.isExcluded(logicalPath)) continue;
3861
+ if (isExcluded(INTERNAL_CONTENT_IGNORE_PATTERNS, logicalPath)) continue;
3862
+ const pattern = matchContentExcludePattern(logicalPath, this.exclude);
3863
+ if (pattern !== void 0) {
3864
+ exclusions.push({ path: logicalPath, pattern });
3865
+ continue;
3866
+ }
3695
3867
  if (directoryEntry.isDirectory()) {
3696
- entries.push(...await this.scanDirectory(filePath, logicalPath));
3868
+ entries.push(
3869
+ ...await this.scanDirectory(filePath, logicalPath, exclusions)
3870
+ );
3697
3871
  continue;
3698
3872
  }
3699
3873
  if (!directoryEntry.isFile()) continue;
@@ -3708,9 +3882,6 @@ var FileSystemContentSource = class {
3708
3882
  }
3709
3883
  return entries;
3710
3884
  }
3711
- isExcluded(logicalPath) {
3712
- return isIgnoredContentPath(logicalPath, this.exclude);
3713
- }
3714
3885
  resolveFilePath(logicalPath) {
3715
3886
  const normalizedPath = normalizeLogicalPath(logicalPath);
3716
3887
  const filePath = path4.resolve(this.contentDirectory, normalizedPath);
@@ -3723,8 +3894,14 @@ var FileSystemContentSource = class {
3723
3894
  };
3724
3895
  function isIgnoredContentPath(logicalPath, exclude = []) {
3725
3896
  if (isExcluded(INTERNAL_CONTENT_IGNORE_PATTERNS, logicalPath)) return true;
3726
- if (isExcluded(exclude, logicalPath)) return true;
3727
- return logicalPath.endsWith(".md") && isExcluded(exclude, logicalPath.slice(0, -3));
3897
+ return matchContentExcludePattern(logicalPath, exclude) !== void 0;
3898
+ }
3899
+ function matchContentExcludePattern(logicalPath, exclude = []) {
3900
+ const direct = exclude.find((pattern) => isExcluded([pattern], logicalPath));
3901
+ if (direct !== void 0) return direct;
3902
+ if (!logicalPath.endsWith(".md")) return void 0;
3903
+ const withoutExtension = logicalPath.slice(0, -3);
3904
+ return exclude.find((pattern) => isExcluded([pattern], withoutExtension));
3728
3905
  }
3729
3906
  function normalizeLogicalPath(logicalPath) {
3730
3907
  const normalizedPath = normalizeContentPath(logicalPath);
@@ -3748,53 +3925,22 @@ function missingContentDirectoryError(directory) {
3748
3925
  return error;
3749
3926
  }
3750
3927
 
3751
- // src/content/content_links.ts
3752
- var WIKILINK_PATTERN = /(!)?\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
3753
- function extractContentLinks(markdown, contentIndex) {
3754
- WIKILINK_PATTERN.lastIndex = 0;
3755
- const links = [];
3756
- for (let match = WIKILINK_PATTERN.exec(markdown); match !== null; match = WIKILINK_PATTERN.exec(markdown)) {
3757
- const rawTarget = match[2]?.trim();
3758
- if (!rawTarget) continue;
3759
- const resolved = contentIndex.get(rawTarget.toLowerCase()) ?? null;
3760
- links.push({
3761
- raw: rawTarget,
3762
- slug: resolved,
3763
- kind: resolved ? linkKind(resolved) : "unresolved",
3764
- embed: match[1] === "!"
3765
- });
3766
- }
3767
- return links;
3768
- }
3769
- function linkKind(value) {
3770
- if (isImagePath(value)) return "image";
3771
- if (isAttachmentPath(value)) return "attachment";
3772
- return "note";
3773
- }
3774
-
3775
3928
  // src/content/content_stable_id.ts
3776
3929
  var MAX_STABLE_CONTENT_ID_LENGTH = 160;
3777
3930
  function resolveContentStableId(frontmatter) {
3778
- const id = readOptionalStableId(frontmatter.id, "id");
3779
- const uid = readOptionalStableId(frontmatter.uid, "uid");
3780
- if (id !== void 0 && uid !== void 0 && id !== uid) {
3781
- throw new Error(
3782
- 'Content frontmatter fields "id" and "uid" must have the same value.'
3783
- );
3784
- }
3785
- return id ?? uid;
3931
+ return readOptionalStableId(frontmatter.id);
3786
3932
  }
3787
- function readOptionalStableId(value, field) {
3933
+ function readOptionalStableId(value) {
3788
3934
  if (value === void 0) return void 0;
3789
3935
  if (typeof value !== "string") {
3790
- throw new Error(`Content frontmatter field "${field}" must be a string.`);
3936
+ throw new Error(`Content frontmatter field "id" must be a string.`);
3791
3937
  }
3792
3938
  if (value.length === 0 || value.length > MAX_STABLE_CONTENT_ID_LENGTH || value.trim() !== value || [...value].some((character) => {
3793
3939
  const code = character.codePointAt(0) ?? 0;
3794
3940
  return code <= 31 || code === 127;
3795
3941
  })) {
3796
3942
  throw new Error(
3797
- `Content frontmatter field "${field}" must be a non-empty, trimmed string of at most ${MAX_STABLE_CONTENT_ID_LENGTH} characters without control characters.`
3943
+ `Content frontmatter field "id" must be a non-empty, trimmed string of at most ${MAX_STABLE_CONTENT_ID_LENGTH} characters without control characters.`
3798
3944
  );
3799
3945
  }
3800
3946
  return value;
@@ -3827,7 +3973,7 @@ var ManifestBuilder = class {
3827
3973
  assets: uniqueAssets(assets)
3828
3974
  };
3829
3975
  }
3830
- build(entries, contentIndex) {
3976
+ build(entries, contentIndex, contentIndexAmbiguities = /* @__PURE__ */ new Map()) {
3831
3977
  const bySlug = new Map(entries.map((entry) => [entry.slug, entry]));
3832
3978
  const byContentId = createContentIdIndex(entries);
3833
3979
  const byAlias = createAliasIndex(entries);
@@ -3868,6 +4014,7 @@ var ManifestBuilder = class {
3868
4014
  outgoingLinks,
3869
4015
  incomingLinks,
3870
4016
  contentIndex,
4017
+ contentIndexAmbiguities,
3871
4018
  assets: [],
3872
4019
  clientEntries: [],
3873
4020
  diagnostics: [],
@@ -3984,10 +4131,10 @@ function buildOutputInventory(manifest, pluginPageOutputs = [], contentOutputDep
3984
4131
  for (const entry of manifest.publicEntries) {
3985
4132
  addOutput(outputs, contentOutput(entry, contentOutputDependencies));
3986
4133
  }
3987
- for (const [path5, redirect] of manifest.publicRedirects) {
4134
+ for (const [path6, redirect] of manifest.publicRedirects) {
3988
4135
  addOutput(outputs, {
3989
4136
  kind: "redirect",
3990
- path: htmlOutputPath(path5),
4137
+ path: htmlOutputPath(path6),
3991
4138
  producer: `content:${redirect.slug}:redirect`,
3992
4139
  dependencies: [{ type: "content", slug: redirect.slug }]
3993
4140
  });
@@ -4101,90 +4248,47 @@ function linkTargets(entry) {
4101
4248
  ).map((link) => link.slug);
4102
4249
  }
4103
4250
  function addOutput(outputs, output) {
4251
+ const existing = outputs.get(output.path);
4252
+ if (existing) {
4253
+ assertNoGeneratedOutputCollision(existing, output);
4254
+ }
4104
4255
  outputs.set(output.path, output);
4105
4256
  }
4257
+ function assertNoGeneratedOutputCollision(existing, incoming) {
4258
+ if (existing.kind !== "generated" && incoming.kind !== "generated") return;
4259
+ if (existing.kind === incoming.kind && existing.producer === incoming.producer)
4260
+ return;
4261
+ throw new Error(
4262
+ `Generated output path "${incoming.path}" collides with ${existing.kind} output owned by "${existing.producer}" and ${incoming.kind} output owned by "${incoming.producer}".`
4263
+ );
4264
+ }
4106
4265
  function normalizePublicPath(pathname) {
4107
- const path5 = `/${pathname.split("/").filter(Boolean).join("/")}`;
4108
- return path5 === "/" ? path5 : path5.replace(/\/$/, "");
4266
+ const path6 = `/${pathname.split("/").filter(Boolean).join("/")}`;
4267
+ return path6 === "/" ? path6 : path6.replace(/\/$/, "");
4109
4268
  }
4110
- function isMarkdownPath3(path5) {
4111
- return path5.endsWith(".md");
4269
+ function isMarkdownPath3(path6) {
4270
+ return path6.endsWith(".md");
4112
4271
  }
4113
- function toSlug3(path5) {
4114
- return path5.replace(/\.md$/, "");
4272
+ function toSlug3(path6) {
4273
+ return path6.replace(/\.md$/, "");
4115
4274
  }
4116
4275
  function compareOutput(left, right) {
4117
4276
  return left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind);
4118
4277
  }
4119
4278
 
4120
- // src/content/publishing.ts
4121
- function resolvePublishingState(frontmatter, options) {
4122
- const explicitVisibility = resolveExplicitVisibility(frontmatter?.visibility);
4123
- const publishAt = resolvePublishAt(frontmatter?.publishAt);
4124
- if (explicitVisibility === "draft")
4125
- return state("draft", false, false, publishAt);
4126
- if (publishAt && publishAt.getTime() > options.buildTime.getTime()) {
4127
- return state("scheduled", false, false, publishAt);
4128
- }
4129
- if (explicitVisibility === "unlisted") {
4130
- return state("unlisted", true, false, publishAt);
4131
- }
4132
- if (explicitVisibility === "public" || publishAt) {
4133
- return state("public", true, true, publishAt);
4134
- }
4135
- const routable = isPublishable(options.strategy, frontmatter);
4136
- return state(routable ? "public" : "draft", routable, routable, publishAt);
4137
- }
4138
- function resolvePublishingBuildTime(input) {
4139
- if (input === void 0) return /* @__PURE__ */ new Date();
4140
- if (input instanceof Date) {
4141
- if (Number.isNaN(input.getTime()))
4142
- throw new Error("Invalid publishing build time.");
4143
- return new Date(input.getTime());
4144
- }
4145
- const parsed = new Date(input);
4146
- if (Number.isNaN(parsed.getTime()))
4147
- throw new Error("Invalid publishing build time.");
4148
- return parsed;
4149
- }
4150
- function resolveExplicitVisibility(value) {
4151
- if (value === void 0) return void 0;
4152
- if (value === "public" || value === "unlisted" || value === "draft")
4153
- return value;
4154
- throw new Error(
4155
- `Invalid content visibility "${String(value)}". Expected public, unlisted, or draft.`
4156
- );
4157
- }
4158
- function resolvePublishAt(value) {
4159
- if (value === void 0) return void 0;
4160
- if (!(typeof value === "string" || value instanceof Date)) {
4161
- throw new Error("Invalid publishAt. Expected an ISO 8601 date string.");
4162
- }
4163
- const parsed = value instanceof Date ? value : new Date(value);
4164
- if (Number.isNaN(parsed.getTime())) {
4165
- throw new Error(
4166
- `Invalid publishAt "${String(value)}". Expected an ISO 8601 date string.`
4167
- );
4168
- }
4169
- return new Date(parsed.getTime());
4170
- }
4171
- function state(visibility, routable, discoverable, publishAt) {
4172
- return {
4173
- visibility,
4174
- routable,
4175
- discoverable,
4176
- ...publishAt ? { publishAt } : {}
4177
- };
4178
- }
4179
-
4180
4279
  // src/content/content_manager.ts
4181
4280
  var CONTENT_PROCESSING_CONCURRENCY = 16;
4182
4281
  var ContentManager = class {
4183
4282
  constructor(content, exclude = [], pipelineOptions = {}) {
4184
4283
  this.pipelineOptions = pipelineOptions;
4284
+ const config = pipelineOptions.config ? {
4285
+ ...pipelineOptions.config,
4286
+ plugins: pipelineOptions.plugins ?? pipelineOptions.config.plugins
4287
+ } : void 0;
4185
4288
  this.pipelineOptions = {
4186
4289
  ...pipelineOptions,
4187
- plugins: pipelineOptions.plugins ?? pipelineOptions.config?.plugins,
4290
+ config,
4291
+ plugins: pipelineOptions.plugins ?? config?.plugins,
4188
4292
  isRoutable: (slug) => this.isRoutable(slug)
4189
4293
  };
4190
4294
  this.publishingBuildTime = resolvePublishingBuildTime(
@@ -4229,6 +4333,7 @@ var ContentManager = class {
4229
4333
  manifestBuilder = new ManifestBuilder();
4230
4334
  pluginRuntime;
4231
4335
  contentIndex = null;
4336
+ contentIndexAmbiguities = /* @__PURE__ */ new Map();
4232
4337
  contentIndexPromise = null;
4233
4338
  contentCache = /* @__PURE__ */ new Map();
4234
4339
  manifest = null;
@@ -4257,15 +4362,10 @@ var ContentManager = class {
4257
4362
  async getContentIndex(preparation) {
4258
4363
  if (this.contentIndex) return this.contentIndex;
4259
4364
  if (!preparation && this.contentIndexPromise)
4260
- return await this.contentIndexPromise;
4261
- if (preparation?.previousState && !hasContentChanges(preparation.changeSet)) {
4262
- this.contentIndex = new Map(
4263
- Object.entries(preparation.previousState.contentIndex)
4264
- );
4265
- return this.contentIndex;
4266
- }
4365
+ return (await this.contentIndexPromise).index;
4267
4366
  if (preparation) {
4268
4367
  this.contentIndex = preparation.currentContentIndex;
4368
+ this.contentIndexAmbiguities = preparation.currentContentIndexAmbiguities;
4269
4369
  return this.contentIndex;
4270
4370
  }
4271
4371
  const promise = this.observability().tracer.span(
@@ -4278,7 +4378,9 @@ var ContentManager = class {
4278
4378
  );
4279
4379
  if (!preparation) this.contentIndexPromise = promise;
4280
4380
  try {
4281
- this.contentIndex = await promise;
4381
+ const built = await promise;
4382
+ this.contentIndex = built.index;
4383
+ this.contentIndexAmbiguities = built.ambiguities;
4282
4384
  return this.contentIndex;
4283
4385
  } finally {
4284
4386
  if (!preparation) this.contentIndexPromise = null;
@@ -4293,48 +4395,55 @@ var ContentManager = class {
4293
4395
  contentPath: `${slug}.md`
4294
4396
  },
4295
4397
  async () => {
4296
- const [contentIndex, rawPost] = await Promise.all([
4297
- this.getContentIndex(),
4298
- this.getPost(slug)
4299
- ]);
4300
- await this.pluginRuntime.startBuild(contentIndex);
4301
- await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
4302
- await this.ensureRoutableSlugs();
4303
- this.pipeline ??= new Pipeline(
4304
- contentIndex,
4305
- await this.locationResolver.getPermalinks(),
4306
- (postSlug) => this.getPost(postSlug),
4307
- this.pipelineOptions,
4308
- this.isBuildTime
4309
- );
4310
- const content = await this.pipeline.execute(rawPost, {
4311
- sourceSlug: slug
4312
- });
4313
- this.processedContentCount += 1;
4314
- if (this.processedContentCount % 100 === 0) {
4315
- this.observability().tracer.event("content.process.sample", {
4316
- processed: this.processedContentCount
4317
- });
4398
+ try {
4399
+ return await this.processContent(slug);
4400
+ } catch (error) {
4401
+ throw attachErrorPath(error, `${slug}.md`);
4318
4402
  }
4319
- await this.pluginRuntime.runPostHook(
4320
- "onPostParsed",
4321
- slug,
4322
- rawPost,
4323
- content,
4324
- contentIndex
4325
- );
4326
- await this.pluginRuntime.runPostHook(
4327
- "onPostProcessed",
4328
- slug,
4329
- rawPost,
4330
- content,
4331
- contentIndex
4332
- );
4333
- this.contentCache.set(slug, content);
4334
- return content;
4335
4403
  }
4336
4404
  );
4337
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
+ }
4338
4447
  async getManifest(options) {
4339
4448
  if (this.manifest) return this.manifest;
4340
4449
  if (this.manifestPromise) return await this.manifestPromise;
@@ -4400,7 +4509,11 @@ var ContentManager = class {
4400
4509
  const manifest = await this.observability().tracer.span(
4401
4510
  "content.graph",
4402
4511
  {},
4403
- () => this.manifestBuilder.build(entries, contentIndex)
4512
+ () => this.manifestBuilder.build(
4513
+ entries,
4514
+ contentIndex,
4515
+ this.contentIndexAmbiguities
4516
+ )
4404
4517
  );
4405
4518
  const cacheManifestEntries = structuredClone(manifest.entries);
4406
4519
  this.locationResolver.populateRedirects(manifest, locations);
@@ -4636,7 +4749,13 @@ var ContentManager = class {
4636
4749
  const routableSlugs = /* @__PURE__ */ new Set();
4637
4750
  await mapConcurrent3(posts, CONTENT_PROCESSING_CONCURRENCY, async (post) => {
4638
4751
  const markdown = await this.getPost(post.slug);
4639
- 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, {
4640
4759
  strategy,
4641
4760
  buildTime: this.publishingBuildTime
4642
4761
  });
@@ -4726,7 +4845,12 @@ function resolveFolderLocation(manifest, folder) {
4726
4845
  if (candidate) return { type: "content", entry: candidate.entry };
4727
4846
  }
4728
4847
  if (candidates.length > 1) return { type: "ambiguous", candidates };
4729
- 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
+ );
4730
4854
  return location ? { type: "generated", pathname: location.pathname } : { type: "none" };
4731
4855
  }
4732
4856
  function resolvePublicFolderLocation(manifest, pathname) {
@@ -4740,12 +4864,15 @@ function resolvePublicFolderLocation(manifest, pathname) {
4740
4864
  );
4741
4865
  return location ? { type: "generated", pathname: location.pathname } : { type: "none" };
4742
4866
  }
4743
- function resolveGeneratedFolderLocation(manifest, folder) {
4867
+ function resolveGeneratedFolderLocation(manifest, folder, language) {
4744
4868
  const normalizedFolder = normalizeFolder2(folder);
4745
4869
  if (!normalizedFolder) return null;
4746
4870
  const candidates = /* @__PURE__ */ new Set();
4747
4871
  const prefix = `${normalizedFolder}/`;
4748
4872
  for (const entry of manifest.publicEntries) {
4873
+ if (language !== void 0 && entry.publicLocation.language !== language) {
4874
+ continue;
4875
+ }
4749
4876
  if (!entry.slug.startsWith(prefix)) continue;
4750
4877
  const remainder = entry.slug.slice(prefix.length).split("/").filter(Boolean);
4751
4878
  const pathname = parentPath(
@@ -4960,7 +5087,7 @@ function clampFinite(value, min, max) {
4960
5087
  // src/content/read_only_content_graph.ts
4961
5088
  async function readOnlyContentGraph(source, locations) {
4962
5089
  const entries = await source.scan();
4963
- const contentIndex = await new ContentIndexBuilder(source).build(entries);
5090
+ const { index: contentIndex, ambiguities: contentIndexAmbiguities } = await new ContentIndexBuilder(source).build(entries);
4964
5091
  const markdownEntries = entries.filter((entry) => entry.path.endsWith(".md")).toSorted((left, right) => left.path.localeCompare(right.path));
4965
5092
  const markdownContents = await Promise.all(
4966
5093
  markdownEntries.map(async (entry) => ({
@@ -4971,7 +5098,11 @@ async function readOnlyContentGraph(source, locations) {
4971
5098
  const graphEntries = markdownContents.map(
4972
5099
  ({ entry, markdown }) => createGraphEntry(entry, markdown, contentIndex, locations)
4973
5100
  );
4974
- return new ManifestBuilder().build(graphEntries, contentIndex).graph;
5101
+ return new ManifestBuilder().build(
5102
+ graphEntries,
5103
+ contentIndex,
5104
+ contentIndexAmbiguities
5105
+ ).graph;
4975
5106
  }
4976
5107
  function createGraphEntry(entry, markdown, contentIndex, locations) {
4977
5108
  const slug = entry.path.replace(/\.md$/, "");
@@ -5005,6 +5136,17 @@ async function readText3(source, entry) {
5005
5136
  return typeof content === "string" ? content : new TextDecoder().decode(content);
5006
5137
  }
5007
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
+
5008
5150
  // src/types/content_manifest.ts
5009
5151
  function appendContentBodySlot(entry, slot, html) {
5010
5152
  if (!html.trim()) return;
@@ -5017,12 +5159,12 @@ ${html}` : html
5017
5159
  }
5018
5160
 
5019
5161
  // src/types/plugin_endpoint.ts
5020
- function defineEndpoint(path5, handler, options = {}) {
5162
+ function defineEndpoint(path6, handler, options = {}) {
5021
5163
  if (options.cacheControl === void 0) {
5022
- return { path: path5, handler };
5164
+ return { path: path6, handler };
5023
5165
  }
5024
5166
  return {
5025
- path: path5,
5167
+ path: path6,
5026
5168
  handler: async (context) => {
5027
5169
  const response = await handler(context);
5028
5170
  const headers = { ...response.headers };
@@ -5071,12 +5213,14 @@ export {
5071
5213
  PluginDependencyError,
5072
5214
  SinkTracer,
5073
5215
  appendContentBodySlot,
5216
+ attachErrorPath,
5074
5217
  attachmentUrl,
5075
5218
  buildContentCollections,
5076
5219
  buildGraphEdges,
5077
5220
  buildOutputInventory,
5078
5221
  calculateReadingTime,
5079
5222
  collectConfigDeprecationDiagnostics,
5223
+ computePipelineFingerprint,
5080
5224
  createClientEntry,
5081
5225
  createContentGraph,
5082
5226
  createDeprecationDiagnostic,
@@ -5095,6 +5239,7 @@ export {
5095
5239
  fingerprintContentEntries,
5096
5240
  formatDeprecationMessage,
5097
5241
  getContentSourceEntry,
5242
+ getEntryLanguage,
5098
5243
  getExtension,
5099
5244
  getResolvedPluginMetadata,
5100
5245
  groupContentEntries,
@@ -5104,11 +5249,13 @@ export {
5104
5249
  isIgnoredContentPath,
5105
5250
  isImagePath,
5106
5251
  isMarkdownPath,
5252
+ isPersistentlyCacheable,
5107
5253
  isPublishable,
5108
5254
  isPublished,
5109
5255
  layoutForceGraph,
5110
5256
  layoutRadialGraph,
5111
5257
  loadContentBuildState,
5258
+ matchContentExcludePattern,
5112
5259
  normalizeContentAssetOutputPath,
5113
5260
  normalizeContentPath,
5114
5261
  normalizeGeneratedOutputPath,
@@ -5118,6 +5265,7 @@ export {
5118
5265
  readContentBuildStateStatus,
5119
5266
  readContentSourceEntry,
5120
5267
  readOnlyContentGraph,
5268
+ resolveBuildOutputDirectory,
5121
5269
  resolveConfig,
5122
5270
  resolveConfigModule,
5123
5271
  resolveContentBuildStatePath,
@@ -5127,10 +5275,12 @@ export {
5127
5275
  resolveFolderEntry,
5128
5276
  resolveFolderLocation,
5129
5277
  resolveGeneratedFolderLocation,
5278
+ resolveManagedStateRoot,
5130
5279
  resolvePlugins,
5131
5280
  resolvePublicFolderLocation,
5132
5281
  resolvePublishingBuildTime,
5133
5282
  resolvePublishingState,
5283
+ selectEntriesByLanguage,
5134
5284
  serializePublicClientConfig,
5135
5285
  stableStringify,
5136
5286
  stripHtml,