scribe-cms 0.0.17 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +10 -0
  2. package/dist/cli/index.cjs +4021 -555
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +4019 -553
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +1514 -229
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +1499 -228
  9. package/dist/index.js.map +1 -1
  10. package/dist/runtime.cjs +447 -33
  11. package/dist/runtime.cjs.map +1 -1
  12. package/dist/runtime.js +446 -32
  13. package/dist/runtime.js.map +1 -1
  14. package/dist/src/config/resolve-config.d.ts.map +1 -1
  15. package/dist/src/core/builtin-fields.d.ts +10 -1
  16. package/dist/src/core/builtin-fields.d.ts.map +1 -1
  17. package/dist/src/core/field.d.ts +70 -0
  18. package/dist/src/core/field.d.ts.map +1 -1
  19. package/dist/src/core/introspect-schema.d.ts +31 -1
  20. package/dist/src/core/introspect-schema.d.ts.map +1 -1
  21. package/dist/src/core/managed-roots.d.ts +15 -0
  22. package/dist/src/core/managed-roots.d.ts.map +1 -0
  23. package/dist/src/core/types.d.ts +56 -4
  24. package/dist/src/core/types.d.ts.map +1 -1
  25. package/dist/src/create-project.d.ts +17 -1
  26. package/dist/src/create-project.d.ts.map +1 -1
  27. package/dist/src/create-scribe.d.ts.map +1 -1
  28. package/dist/src/delete/execute.d.ts +21 -0
  29. package/dist/src/delete/execute.d.ts.map +1 -0
  30. package/dist/src/delete/plan.d.ts +85 -0
  31. package/dist/src/delete/plan.d.ts.map +1 -0
  32. package/dist/src/delete/render-text.d.ts +7 -0
  33. package/dist/src/delete/render-text.d.ts.map +1 -0
  34. package/dist/src/hash/page-hash.d.ts +9 -0
  35. package/dist/src/hash/page-hash.d.ts.map +1 -1
  36. package/dist/src/index.d.ts +12 -3
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/inline/resolve-tokens.d.ts +39 -0
  39. package/dist/src/inline/resolve-tokens.d.ts.map +1 -0
  40. package/dist/src/inline/tokens.d.ts +95 -0
  41. package/dist/src/inline/tokens.d.ts.map +1 -0
  42. package/dist/src/loader/create-loader.d.ts +7 -1
  43. package/dist/src/loader/create-loader.d.ts.map +1 -1
  44. package/dist/src/loader/resolve-assets.d.ts +16 -0
  45. package/dist/src/loader/resolve-assets.d.ts.map +1 -0
  46. package/dist/src/runtime.d.ts +1 -1
  47. package/dist/src/runtime.d.ts.map +1 -1
  48. package/dist/src/storage/translations.d.ts +8 -0
  49. package/dist/src/storage/translations.d.ts.map +1 -1
  50. package/dist/src/translate/resolve-translate-config.d.ts.map +1 -1
  51. package/dist/src/translate/response-schema.d.ts +7 -1
  52. package/dist/src/translate/response-schema.d.ts.map +1 -1
  53. package/dist/src/translate/translate-core.d.ts +6 -0
  54. package/dist/src/translate/translate-core.d.ts.map +1 -1
  55. package/dist/src/translate/validate-translation.d.ts.map +1 -1
  56. package/dist/src/translate/worklist.d.ts +8 -1
  57. package/dist/src/translate/worklist.d.ts.map +1 -1
  58. package/dist/src/validate/validate-assets.d.ts +26 -2
  59. package/dist/src/validate/validate-assets.d.ts.map +1 -1
  60. package/dist/src/validate/validate-inline-tokens.d.ts +10 -0
  61. package/dist/src/validate/validate-inline-tokens.d.ts.map +1 -0
  62. package/dist/src/validate/validate-project.d.ts.map +1 -1
  63. package/dist/src/validate/validate-relations.d.ts +2 -1
  64. package/dist/src/validate/validate-relations.d.ts.map +1 -1
  65. package/dist/studio/asset-serve.d.ts +39 -0
  66. package/dist/studio/asset-serve.d.ts.map +1 -0
  67. package/dist/studio/asset-views.d.ts +4 -0
  68. package/dist/studio/asset-views.d.ts.map +1 -0
  69. package/dist/studio/content-views.d.ts +85 -0
  70. package/dist/studio/content-views.d.ts.map +1 -0
  71. package/dist/studio/introspect-fields.d.ts +111 -0
  72. package/dist/studio/introspect-fields.d.ts.map +1 -0
  73. package/dist/studio/mdx-preview.d.ts +3 -0
  74. package/dist/studio/mdx-preview.d.ts.map +1 -0
  75. package/dist/studio/preview-tokens.d.ts +16 -0
  76. package/dist/studio/preview-tokens.d.ts.map +1 -0
  77. package/dist/studio/search.d.ts +34 -0
  78. package/dist/studio/search.d.ts.map +1 -0
  79. package/dist/studio/server.cjs +4699 -304
  80. package/dist/studio/server.cjs.map +1 -1
  81. package/dist/studio/server.d.ts.map +1 -1
  82. package/dist/studio/server.js +4697 -302
  83. package/dist/studio/server.js.map +1 -1
  84. package/dist/studio/shared.d.ts +20 -0
  85. package/dist/studio/shared.d.ts.map +1 -0
  86. package/dist/studio/studio-cache.d.ts +93 -0
  87. package/dist/studio/studio-cache.d.ts.map +1 -0
  88. package/package.json +2 -2
package/dist/runtime.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  var path = require('path');
4
4
  var zod = require('zod');
5
- var fs2 = require('fs');
5
+ var fs3 = require('fs');
6
6
  var matter = require('gray-matter');
7
7
  var Database = require('better-sqlite3');
8
8
  var remarkMdx = require('remark-mdx');
@@ -12,7 +12,7 @@ var unified = require('unified');
12
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
 
14
14
  var path__default = /*#__PURE__*/_interopDefault(path);
15
- var fs2__default = /*#__PURE__*/_interopDefault(fs2);
15
+ var fs3__default = /*#__PURE__*/_interopDefault(fs3);
16
16
  var matter__default = /*#__PURE__*/_interopDefault(matter);
17
17
  var Database__default = /*#__PURE__*/_interopDefault(Database);
18
18
  var remarkMdx__default = /*#__PURE__*/_interopDefault(remarkMdx);
@@ -142,7 +142,15 @@ function resolveConfig(input, baseDir) {
142
142
  const projectRoot = path__default.default.resolve(process.cwd(), raw.rootDir);
143
143
  const contentRoot = path__default.default.resolve(projectRoot, raw.contentDir ?? "content");
144
144
  const storePath = path__default.default.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
145
- const assetsPath = raw.assetsDir ? path__default.default.resolve(projectRoot, raw.assetsDir) : void 0;
145
+ let assetsPath;
146
+ let assets;
147
+ if (raw.assets !== void 0 || raw.assetsDir !== void 0) {
148
+ const dir = raw.assets?.dir ?? raw.assetsDir ?? "public";
149
+ const publicPath = raw.assets?.publicPath ?? "/";
150
+ const managedDirs = raw.assets?.managedDirs ?? [];
151
+ assetsPath = path__default.default.resolve(projectRoot, dir);
152
+ assets = { assetsPath, publicPath, managedDirs };
153
+ }
146
154
  const seenIds = /* @__PURE__ */ new Set();
147
155
  const types = raw.types.map((type) => {
148
156
  if (seenIds.has(type.id)) {
@@ -157,13 +165,17 @@ function resolveConfig(input, baseDir) {
157
165
  contentDir: type.contentDir ?? type.id,
158
166
  label: type.label ?? type.id.charAt(0).toUpperCase() + type.id.slice(1),
159
167
  slugStrategy: type.slugStrategy ?? "fixed",
160
- indexFallback: type.indexFallback ?? (type.path ? "en" : "none")
168
+ indexFallback: type.indexFallback ?? (type.path ? "en" : "none"),
169
+ // `body` defaults to true (fully backwards compatible). A resolved type
170
+ // always carries an explicit boolean so consumers read one source of truth.
171
+ body: type.body ?? true
161
172
  };
162
173
  });
163
174
  const config = {
164
175
  rootDir: contentRoot,
165
176
  storePath,
166
177
  assetsPath,
178
+ assets,
167
179
  locales: [...raw.locales],
168
180
  defaultLocale,
169
181
  localeRouting,
@@ -199,6 +211,7 @@ function deriveLocaleFallbacks(locales, defaultLocale) {
199
211
  }
200
212
  var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
201
213
  var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
214
+ var ASSET_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/assetMeta");
202
215
  function getFieldKind(schema) {
203
216
  const tagged = schema;
204
217
  return tagged[FIELD_KIND] ?? "structural";
@@ -226,6 +239,29 @@ function getRelationTarget(schema) {
226
239
  }
227
240
  return null;
228
241
  }
242
+ function getAssetMeta(schema) {
243
+ let current = schema;
244
+ for (let i = 0; i < 8; i++) {
245
+ const tagged = current;
246
+ if (tagged[ASSET_META]) {
247
+ return tagged[ASSET_META];
248
+ }
249
+ if (typeof tagged.unwrap === "function") {
250
+ current = tagged.unwrap();
251
+ continue;
252
+ }
253
+ if (typeof tagged.removeDefault === "function") {
254
+ current = tagged.removeDefault();
255
+ continue;
256
+ }
257
+ if (tagged._def?.innerType) {
258
+ current = tagged._def.innerType;
259
+ continue;
260
+ }
261
+ break;
262
+ }
263
+ return null;
264
+ }
229
265
  function peelOptionalWrappers(schema) {
230
266
  let current = schema;
231
267
  for (let i = 0; i < 8; i++) {
@@ -259,7 +295,23 @@ function introspectSchema(schema, prefix = []) {
259
295
  kind: "relation",
260
296
  relationTarget: relation.typeId,
261
297
  relationMultiple: relation.multiple,
262
- relationOptional: relation.optional
298
+ relationOptional: relation.optional,
299
+ relationOnTargetDelete: relation.onTargetDelete
300
+ }
301
+ ];
302
+ }
303
+ const asset = getAssetMeta(schema);
304
+ if (asset && prefix.length > 0) {
305
+ return [
306
+ {
307
+ path: prefix,
308
+ kind: "asset",
309
+ assetDir: asset.dir,
310
+ assetTemplate: asset.template,
311
+ assetFormats: asset.formats,
312
+ assetMaxKB: asset.maxKB,
313
+ assetOptional: asset.optional,
314
+ assetOnDelete: asset.onDelete
263
315
  }
264
316
  ];
265
317
  }
@@ -337,7 +389,7 @@ function pickTranslatable(data, schema) {
337
389
  return extractByPaths(data, translatablePaths);
338
390
  }
339
391
  function pickStructural(data, schema) {
340
- const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation").map((f) => f.path);
392
+ const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation" || f.kind === "asset").map((f) => f.path);
341
393
  return extractByPaths(data, structuralPaths);
342
394
  }
343
395
  function mergeStructuralOntoLocale(localeData, enData, schema) {
@@ -352,7 +404,7 @@ function deepMerge(base, overlay) {
352
404
  out[key] = deepMerge(out[key], value);
353
405
  } else if (Array.isArray(value) && Array.isArray(out[key])) {
354
406
  out[key] = mergeArrayOverlay(out[key], value);
355
- } else {
407
+ } else if (value !== void 0) {
356
408
  out[key] = value;
357
409
  }
358
410
  }
@@ -370,6 +422,9 @@ function mergeArrayOverlay(base, overlay) {
370
422
  function listRelationFields(schema) {
371
423
  return introspectSchema(schema).filter((f) => f.kind === "relation");
372
424
  }
425
+ function listAssetFields(schema) {
426
+ return introspectSchema(schema).filter((f) => f.kind === "asset");
427
+ }
373
428
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
374
429
  var slugPatternSchema = zod.z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
375
430
  var isoDateSchema = zod.z.string().regex(/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/, "Use ISO date YYYY-MM-DD or full ISO 8601");
@@ -450,12 +505,18 @@ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
450
505
  delete rest[internalKey];
451
506
  }
452
507
  }
508
+ let vars;
509
+ if (rest.vars !== void 0) {
510
+ vars = rest.vars;
511
+ delete rest.vars;
512
+ }
453
513
  return {
454
514
  builtin: {
455
515
  publishedAt,
456
516
  updatedAt,
457
517
  noindex,
458
- canonicalPathOverride
518
+ canonicalPathOverride,
519
+ vars
459
520
  },
460
521
  rest,
461
522
  issues
@@ -485,6 +546,7 @@ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, loc
485
546
  if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
486
547
  out.noindex = doc.noindex;
487
548
  out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
549
+ if (doc.vars !== void 0) out.vars = doc.vars;
488
550
  return out;
489
551
  }
490
552
  function seoFieldsFromEn(enDoc) {
@@ -574,7 +636,7 @@ function resolveStorePath(config) {
574
636
  function openStore(config, mode = "readwrite") {
575
637
  const storePath = resolveStorePath(config);
576
638
  if (mode === "readwrite") {
577
- fs2__default.default.mkdirSync(path__default.default.dirname(storePath), { recursive: true });
639
+ fs3__default.default.mkdirSync(path__default.default.dirname(storePath), { recursive: true });
578
640
  }
579
641
  const db = new Database__default.default(storePath, { readonly: mode === "readonly" });
580
642
  if (mode === "readwrite") {
@@ -613,6 +675,9 @@ function migrate(db) {
613
675
  function listTranslationsForType(db, contentType) {
614
676
  return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
615
677
  }
678
+ function bulkLoadTranslations(db) {
679
+ return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
680
+ }
616
681
 
617
682
  // src/translate/normalize-mdx-body.ts
618
683
  function needsMdxEscapeNormalization(body) {
@@ -729,6 +794,284 @@ function prepareTranslatedMdxBody(body) {
729
794
  };
730
795
  }
731
796
 
797
+ // src/loader/resolve-assets.ts
798
+ function joinPublicPath(publicPath, webPath) {
799
+ if (publicPath === "" || publicPath === "/") return webPath;
800
+ const suffix = webPath.startsWith("/") ? webPath : `/${webPath}`;
801
+ return publicPath.replace(/\/+$/, "") + suffix;
802
+ }
803
+ function materializeTemplate(template, enSlug) {
804
+ return template.split("{slug}").join(enSlug);
805
+ }
806
+ function setAssetAtPath(container, path6, transform) {
807
+ const [head, ...rest] = path6;
808
+ if (head === void 0) return;
809
+ if (rest.length === 0) {
810
+ const current = container[head];
811
+ const next = transform(typeof current === "string" ? current : void 0);
812
+ if (next !== void 0) container[head] = next;
813
+ return;
814
+ }
815
+ if (rest[0] === "*") {
816
+ const arr = container[head];
817
+ if (!Array.isArray(arr)) return;
818
+ for (const item of arr) {
819
+ if (item && typeof item === "object" && !Array.isArray(item)) {
820
+ setAssetAtPath(item, rest.slice(1), transform);
821
+ }
822
+ }
823
+ return;
824
+ }
825
+ const child = container[head];
826
+ if (child && typeof child === "object" && !Array.isArray(child)) {
827
+ setAssetAtPath(child, rest, transform);
828
+ }
829
+ }
830
+ function resolveDocumentAssets(doc, assetFields, assets) {
831
+ const frontmatter = doc.frontmatter;
832
+ for (const f of assetFields) {
833
+ setAssetAtPath(frontmatter, f.path, (current) => {
834
+ let value = current;
835
+ if (value === void 0) {
836
+ if (!f.assetTemplate) return void 0;
837
+ value = materializeTemplate(f.assetTemplate, doc.enSlug);
838
+ }
839
+ return joinPublicPath(assets.publicPath, value);
840
+ });
841
+ }
842
+ }
843
+
844
+ // src/inline/tokens.ts
845
+ var TOKEN_OPEN = "${{";
846
+ var ESCAPE_SEQUENCE = "$\\{{";
847
+ var ESCAPE_RESULT = "${{";
848
+ function placeholderMarker(n) {
849
+ return `%%${n}%%`;
850
+ }
851
+ function findTokenEnd(body, start) {
852
+ let i = start;
853
+ let inString = false;
854
+ while (i < body.length) {
855
+ const ch = body[i];
856
+ if (ch === "\n") return -1;
857
+ if (inString) {
858
+ if (ch === "\\") {
859
+ i += 2;
860
+ continue;
861
+ }
862
+ if (ch === '"') inString = false;
863
+ i += 1;
864
+ continue;
865
+ }
866
+ if (ch === '"') {
867
+ inString = true;
868
+ i += 1;
869
+ continue;
870
+ }
871
+ if (ch === "}" && body[i + 1] === "}") return i;
872
+ i += 1;
873
+ }
874
+ return -1;
875
+ }
876
+ function parseTokenInner(inner) {
877
+ const colon = inner.indexOf(":");
878
+ if (colon === -1) {
879
+ return { ok: false, reason: `missing kind separator ":"` };
880
+ }
881
+ const kind = inner.slice(0, colon);
882
+ const rest = inner.slice(colon + 1);
883
+ switch (kind) {
884
+ case "static": {
885
+ let text;
886
+ try {
887
+ text = JSON.parse(rest);
888
+ } catch {
889
+ return { ok: false, reason: `static value is not a valid JSON string` };
890
+ }
891
+ if (typeof text !== "string") {
892
+ return { ok: false, reason: `static value must be a JSON string` };
893
+ }
894
+ return { ok: true, token: { kind: "static", text } };
895
+ }
896
+ case "relation": {
897
+ const parts = rest.split(":");
898
+ if (parts.length !== 3) {
899
+ return {
900
+ ok: false,
901
+ reason: `relation must be relation:<typeId>:<enSlug>:href or relation:<typeId>:<enSlug>:slug`
902
+ };
903
+ }
904
+ const [targetTypeId, enSlug, mode] = parts;
905
+ if (!targetTypeId || !enSlug) {
906
+ return { ok: false, reason: `relation is missing a typeId or enSlug` };
907
+ }
908
+ if (mode !== "href" && mode !== "slug") {
909
+ return { ok: false, reason: `relation mode must be "href" or "slug" (got "${mode}")` };
910
+ }
911
+ return {
912
+ ok: true,
913
+ token: {
914
+ kind: "relation",
915
+ targetTypeId,
916
+ enSlug,
917
+ mode
918
+ }
919
+ };
920
+ }
921
+ case "asset": {
922
+ if (!rest.startsWith("/")) {
923
+ return { ok: false, reason: `asset path must start with "/" (got "${rest}")` };
924
+ }
925
+ return { ok: true, token: { kind: "asset", webPath: rest } };
926
+ }
927
+ case "var": {
928
+ if (!rest) return { ok: false, reason: `var is missing a key` };
929
+ return { ok: true, token: { kind: "var", key: rest } };
930
+ }
931
+ default:
932
+ return { ok: false, reason: `unknown token kind "${kind}"` };
933
+ }
934
+ }
935
+ function extractInlineTokens(body) {
936
+ const tokens = [];
937
+ const malformed = [];
938
+ let out = "";
939
+ let i = 0;
940
+ while (i < body.length) {
941
+ const open = body.indexOf(TOKEN_OPEN, i);
942
+ if (open === -1) {
943
+ out += body.slice(i);
944
+ break;
945
+ }
946
+ out += body.slice(i, open);
947
+ const contentStart = open + TOKEN_OPEN.length;
948
+ const end = findTokenEnd(body, contentStart);
949
+ if (end === -1) {
950
+ malformed.push({
951
+ raw: body.slice(open),
952
+ reason: `unterminated token (missing "}}")`,
953
+ index: open
954
+ });
955
+ out += body.slice(open);
956
+ break;
957
+ }
958
+ const raw = body.slice(open, end + 2);
959
+ const inner = body.slice(contentStart, end);
960
+ const parsed = parseTokenInner(inner);
961
+ if (parsed.ok) {
962
+ tokens.push({ ...parsed.token, raw });
963
+ out += placeholderMarker(tokens.length);
964
+ } else {
965
+ malformed.push({ raw, reason: parsed.reason, index: open });
966
+ out += raw;
967
+ }
968
+ i = end + 2;
969
+ }
970
+ return { placeholderBody: out, tokens, malformed };
971
+ }
972
+ function fillPlaceholders(body, resolvedValues) {
973
+ return body.replace(/%%(\d+)%%/g, (match, n) => {
974
+ const idx = Number(n) - 1;
975
+ return idx >= 0 && idx < resolvedValues.length ? resolvedValues[idx] : match;
976
+ });
977
+ }
978
+ function unescapeInlineTokens(body) {
979
+ return body.split(ESCAPE_SEQUENCE).join(ESCAPE_RESULT);
980
+ }
981
+
982
+ // src/inline/resolve-tokens.ts
983
+ function slugKey(typeId, enSlug, locale) {
984
+ return `${typeId}\0${enSlug}\0${locale}`;
985
+ }
986
+ function isStringRecord(value) {
987
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
988
+ return Object.values(value).every((v) => typeof v === "string");
989
+ }
990
+ function createInlineResolver(config, options = {}) {
991
+ const linkStyle = options.linkStyle ?? "app";
992
+ const exportExtension = options.exportExtension ?? ".md";
993
+ const urlBuilder = createUrlBuilder(config);
994
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
995
+ const storePath = resolveStorePath(config);
996
+ let slugCache = null;
997
+ const localizedSlugIndex = () => {
998
+ let mtime = 0;
999
+ try {
1000
+ mtime = fs3__default.default.statSync(storePath).mtimeMs;
1001
+ } catch {
1002
+ mtime = 0;
1003
+ }
1004
+ if (slugCache && slugCache.mtime === mtime) return slugCache.index;
1005
+ const index = /* @__PURE__ */ new Map();
1006
+ try {
1007
+ const db = openStore(config, "readonly");
1008
+ try {
1009
+ for (const row of bulkLoadTranslations(db)) {
1010
+ index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
1011
+ }
1012
+ } finally {
1013
+ db.close();
1014
+ }
1015
+ } catch {
1016
+ }
1017
+ slugCache = { mtime, index };
1018
+ return index;
1019
+ };
1020
+ const localizedSlug = (typeId, enSlug, locale) => {
1021
+ if (locale === config.defaultLocale) return enSlug;
1022
+ const index = localizedSlugIndex();
1023
+ const direct = index.get(slugKey(typeId, enSlug, locale));
1024
+ if (direct) return direct;
1025
+ for (const fb of config.localeFallbacks?.[locale] ?? []) {
1026
+ const hit = index.get(slugKey(typeId, enSlug, fb));
1027
+ if (hit) return hit;
1028
+ }
1029
+ return enSlug;
1030
+ };
1031
+ return {
1032
+ resolve(token, enFrontmatter, locale) {
1033
+ switch (token.kind) {
1034
+ case "static":
1035
+ return token.text;
1036
+ case "var": {
1037
+ const vars = enFrontmatter.vars;
1038
+ if (isStringRecord(vars) && typeof vars[token.key] === "string") {
1039
+ return vars[token.key];
1040
+ }
1041
+ return "";
1042
+ }
1043
+ case "asset": {
1044
+ const assets = config.assets;
1045
+ return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
1046
+ }
1047
+ case "relation": {
1048
+ const type = typeById.get(token.targetTypeId);
1049
+ if (!type) return "";
1050
+ if (token.mode === "slug") return token.enSlug;
1051
+ if (!isRoutableType(type)) return "";
1052
+ const slug = localizedSlug(token.targetTypeId, token.enSlug, locale);
1053
+ if (linkStyle === "export") {
1054
+ return urlBuilder.resolvePath(type.path, `${slug}${exportExtension}`, locale);
1055
+ }
1056
+ return type.path.replace("{slug}", slug);
1057
+ }
1058
+ }
1059
+ }
1060
+ };
1061
+ }
1062
+ function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
1063
+ const { placeholderBody, tokens } = extractInlineTokens(rawBody);
1064
+ if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
1065
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
1066
+ return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
1067
+ }
1068
+ function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
1069
+ const { tokens } = extractInlineTokens(enRawBody);
1070
+ if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
1071
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
1072
+ return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
1073
+ }
1074
+
732
1075
  // src/loader/normalize-en.ts
733
1076
  function normalizeEnFrontmatter(data) {
734
1077
  const out = { ...data };
@@ -754,14 +1097,14 @@ function isPostFile(name) {
754
1097
  return isPublishableContentFile(name);
755
1098
  }
756
1099
  function listEnFiles(contentDir) {
757
- if (!fs2__default.default.existsSync(contentDir)) return [];
758
- return fs2__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path__default.default.join(contentDir, f));
1100
+ if (!fs3__default.default.existsSync(contentDir)) return [];
1101
+ return fs3__default.default.readdirSync(contentDir).filter(isPostFile).map((f) => path__default.default.join(contentDir, f));
759
1102
  }
760
1103
  function slugFromPath(filePath) {
761
1104
  return path__default.default.basename(filePath).replace(/\.(md|mdx)$/, "");
762
1105
  }
763
1106
  function parseEnMdx(filePath, config, type) {
764
- const raw = fs2__default.default.readFileSync(filePath, "utf8");
1107
+ const raw = fs3__default.default.readFileSync(filePath, "utf8");
765
1108
  const parsed = matter__default.default(raw);
766
1109
  const slug = slugFromPath(filePath);
767
1110
  const normalized = normalizeEnFrontmatter(parsed.data);
@@ -800,6 +1143,7 @@ function parseEnMdx(filePath, config, type) {
800
1143
  updatedAt: builtin.updatedAt,
801
1144
  noindex: builtin.noindex,
802
1145
  canonicalPathOverride: builtin.canonicalPathOverride,
1146
+ vars: builtin.vars,
803
1147
  slug,
804
1148
  locale: config.defaultLocale
805
1149
  },
@@ -824,12 +1168,15 @@ function parseEnMdx(filePath, config, type) {
824
1168
  noindex: builtin.noindex,
825
1169
  canonicalPathOverride: builtin.canonicalPathOverride,
826
1170
  frontmatter,
827
- content: parsed.content,
1171
+ // Bodyless types (`body: false`) never carry a body: the loader skips the
1172
+ // MDX body entirely so runtimes, exports, and hashing all see an empty body.
1173
+ // A stray body is reported separately by `scribe validate`.
1174
+ content: type.body === false ? "" : parsed.content,
828
1175
  filePath
829
1176
  };
830
1177
  return { document, issues };
831
1178
  }
832
- function buildDocumentFromTranslation(row, enDoc, type, config) {
1179
+ function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
833
1180
  const localeFm = JSON.parse(row.frontmatter_json);
834
1181
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
835
1182
  const seo = seoFieldsFromEn(enDoc);
@@ -840,6 +1187,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
840
1187
  config.defaultLocale,
841
1188
  config.localeRouting
842
1189
  );
1190
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1191
+ const content = inlineResolver ? fillTranslatedInlineBody(
1192
+ preparedBody,
1193
+ enDoc.content,
1194
+ enDoc.frontmatter,
1195
+ row.locale,
1196
+ inlineResolver
1197
+ ) : preparedBody;
843
1198
  return {
844
1199
  slug: row.slug,
845
1200
  enSlug: row.en_slug,
@@ -849,14 +1204,16 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
849
1204
  noindex: seo.noindex,
850
1205
  canonicalPathOverride: seo.canonicalPathOverride,
851
1206
  frontmatter,
852
- content: prepareTranslatedMdxBody(row.body).body
1207
+ content
853
1208
  };
854
1209
  }
855
1210
  var DEV_REVALIDATE_MS = 1500;
856
- function createContentLoader(config, type) {
1211
+ var contentVersion = 0;
1212
+ function createContentLoader(config, type, options = {}) {
857
1213
  let cached = null;
858
1214
  let signature = "";
859
1215
  let lastCheck = 0;
1216
+ let builtVersion = contentVersion;
860
1217
  const contentDir = path__default.default.join(
861
1218
  /* turbopackIgnore: true */
862
1219
  config.rootDir,
@@ -864,18 +1221,34 @@ function createContentLoader(config, type) {
864
1221
  );
865
1222
  const storePath = resolveStorePath(config);
866
1223
  const isProd = process.env.NODE_ENV === "production";
1224
+ const assets = config.assets;
1225
+ const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
1226
+ const resolveAssets = (doc) => {
1227
+ if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
1228
+ };
1229
+ const inlineResolver = options.inlineResolver;
1230
+ const resolveEnInline = (doc) => {
1231
+ if (inlineResolver) {
1232
+ doc.content = substituteEnInlineBody(
1233
+ doc.content,
1234
+ doc.frontmatter,
1235
+ config.defaultLocale,
1236
+ inlineResolver
1237
+ );
1238
+ }
1239
+ };
867
1240
  function computeSignature() {
868
1241
  const files = listEnFiles(contentDir);
869
1242
  let newest = 0;
870
1243
  for (const f of files) {
871
1244
  try {
872
- newest = Math.max(newest, fs2__default.default.statSync(f).mtimeMs);
1245
+ newest = Math.max(newest, fs3__default.default.statSync(f).mtimeMs);
873
1246
  } catch {
874
1247
  }
875
1248
  }
876
1249
  let store = 0;
877
1250
  try {
878
- store = fs2__default.default.statSync(storePath).mtimeMs;
1251
+ store = fs3__default.default.statSync(storePath).mtimeMs;
879
1252
  } catch {
880
1253
  }
881
1254
  return `${files.length}:${newest}:${store}`;
@@ -921,16 +1294,28 @@ function createContentLoader(config, type) {
921
1294
  for (const row of rowsByLocale.get(locale) ?? []) {
922
1295
  const enDoc = englishBySlug.get(row.en_slug);
923
1296
  if (!enDoc) continue;
924
- const doc = buildDocumentFromTranslation(row, enDoc, type, config);
1297
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
1298
+ resolveAssets(doc);
925
1299
  bySlug.set(doc.slug, doc);
926
1300
  byEnSlug.set(row.en_slug, doc);
927
1301
  }
928
1302
  out.set(locale, { bySlug, byEnSlug });
929
1303
  }
1304
+ for (const doc of englishBySlug.values()) {
1305
+ resolveAssets(doc);
1306
+ resolveEnInline(doc);
1307
+ }
930
1308
  return out;
931
1309
  }
932
1310
  return () => {
933
1311
  if (cached) {
1312
+ if (contentVersion !== builtVersion) {
1313
+ cached = build();
1314
+ builtVersion = contentVersion;
1315
+ lastCheck = Date.now();
1316
+ signature = isProd ? "" : computeSignature();
1317
+ return cached;
1318
+ }
934
1319
  if (isProd) return cached;
935
1320
  const now = Date.now();
936
1321
  if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
@@ -943,6 +1328,7 @@ function createContentLoader(config, type) {
943
1328
  }
944
1329
  lastCheck = Date.now();
945
1330
  cached = build();
1331
+ builtVersion = contentVersion;
946
1332
  signature = isProd ? "" : computeSignature();
947
1333
  return cached;
948
1334
  };
@@ -1032,8 +1418,11 @@ function comparatorFor(orderBy) {
1032
1418
  return (a, b) => a.slug.localeCompare(b.slug);
1033
1419
  }
1034
1420
  }
1035
- function buildRuntime(config, type, getRuntime) {
1036
- const load = createContentLoader(config, type);
1421
+ function buildRuntime(config, type, getRuntime, options = {}) {
1422
+ const load = createContentLoader(config, type, {
1423
+ resolveAssets: options.resolveAssets,
1424
+ inlineResolver: options.inlineResolver
1425
+ });
1037
1426
  const relationFields = new Map(
1038
1427
  listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1039
1428
  );
@@ -1050,12 +1439,12 @@ function buildRuntime(config, type, getRuntime) {
1050
1439
  id: type.id,
1051
1440
  config: type,
1052
1441
  load,
1053
- list(locale = config.defaultLocale, options = {}) {
1442
+ list(locale = config.defaultLocale, options2 = {}) {
1054
1443
  const idx = load().get(locale);
1055
1444
  if (!idx) return [];
1056
1445
  const docs = Array.from(idx.bySlug.values());
1057
- docs.sort(comparatorFor(options.orderBy ?? type.orderBy ?? "slug"));
1058
- return options.limit !== void 0 ? docs.slice(0, options.limit) : docs;
1446
+ docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
1447
+ return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
1059
1448
  },
1060
1449
  get(slug, locale = config.defaultLocale) {
1061
1450
  return load().get(locale)?.bySlug.get(slug) ?? null;
@@ -1068,7 +1457,7 @@ function buildRuntime(config, type, getRuntime) {
1068
1457
  load(),
1069
1458
  type,
1070
1459
  config.localeRouting,
1071
- config.localeFallbacks[locale] ?? []
1460
+ config.localeFallbacks?.[locale] ?? []
1072
1461
  );
1073
1462
  if (result.document && type.path) {
1074
1463
  return {
@@ -1082,15 +1471,15 @@ function buildRuntime(config, type, getRuntime) {
1082
1471
  }
1083
1472
  return result;
1084
1473
  },
1085
- staticParams(options = {}) {
1474
+ staticParams(options2 = {}) {
1086
1475
  assertRoutable("staticParams");
1087
1476
  const all = load();
1088
1477
  const enIdx = all.get(config.defaultLocale);
1089
1478
  if (!enIdx) return [];
1090
1479
  const params = [];
1091
- for (const locale of options.locales ?? config.locales) {
1480
+ for (const locale of options2.locales ?? config.locales) {
1092
1481
  const localeIdx = all.get(locale);
1093
- const fallbacks = config.localeFallbacks[locale] ?? [];
1482
+ const fallbacks = config.localeFallbacks?.[locale] ?? [];
1094
1483
  for (const doc of enIdx.bySlug.values()) {
1095
1484
  let slug;
1096
1485
  if (locale === config.defaultLocale) {
@@ -1169,7 +1558,7 @@ function buildRuntime(config, type, getRuntime) {
1169
1558
  };
1170
1559
  return runtime;
1171
1560
  }
1172
- function createProject(config) {
1561
+ function createProject(config, options = {}) {
1173
1562
  const runtimes = /* @__PURE__ */ new Map();
1174
1563
  const getRuntime = (id) => {
1175
1564
  const runtime = runtimes.get(id);
@@ -1178,8 +1567,18 @@ function createProject(config) {
1178
1567
  }
1179
1568
  return runtime;
1180
1569
  };
1570
+ const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config, {
1571
+ linkStyle: options.inlineLinkStyle,
1572
+ exportExtension: options.exportLinkExtension
1573
+ }) : void 0;
1181
1574
  for (const type of config.types) {
1182
- runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1575
+ runtimes.set(
1576
+ type.id,
1577
+ buildRuntime(config, type, getRuntime, {
1578
+ resolveAssets: options.resolveAssets,
1579
+ inlineResolver
1580
+ })
1581
+ );
1183
1582
  }
1184
1583
  return {
1185
1584
  config,
@@ -1272,10 +1671,10 @@ function redirectsFilePath(config, type) {
1272
1671
  }
1273
1672
  function loadTypeRedirectsFile(config, type) {
1274
1673
  const filePath = redirectsFilePath(config, type);
1275
- if (!fs2__default.default.existsSync(filePath)) return null;
1674
+ if (!fs3__default.default.existsSync(filePath)) return null;
1276
1675
  let raw;
1277
1676
  try {
1278
- raw = JSON.parse(fs2__default.default.readFileSync(filePath, "utf8"));
1677
+ raw = JSON.parse(fs3__default.default.readFileSync(filePath, "utf8"));
1279
1678
  } catch (error) {
1280
1679
  throw new Error(
1281
1680
  `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
@@ -1431,7 +1830,11 @@ async function generateSitemap(project, options) {
1431
1830
  // src/create-scribe.ts
1432
1831
  function createScribe(input) {
1433
1832
  const config = resolveConfig(input);
1434
- const project = createProject(config);
1833
+ const project = createProject(config, {
1834
+ resolveAssets: true,
1835
+ resolveInlineTokens: true,
1836
+ inlineLinkStyle: "app"
1837
+ });
1435
1838
  const scribe = {
1436
1839
  config,
1437
1840
  project,
@@ -1440,6 +1843,17 @@ function createScribe(input) {
1440
1843
  listRoutableTypes: project.listRoutableTypes,
1441
1844
  sitemap(options) {
1442
1845
  return generateSitemap(project, options);
1846
+ },
1847
+ assets: {
1848
+ url(ref, opts) {
1849
+ if (opts && Object.keys(opts).length > 0) {
1850
+ throw new Error(
1851
+ `scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
1852
+ );
1853
+ }
1854
+ const assetsConfig = config.assets;
1855
+ return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
1856
+ }
1443
1857
  }
1444
1858
  };
1445
1859
  for (const type of config.types) {