scribe-cms 0.0.17 → 0.0.20

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 +4013 -555
  3. package/dist/cli/index.cjs.map +1 -1
  4. package/dist/cli/index.js +4011 -553
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.cjs +1502 -229
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +1487 -228
  9. package/dist/index.js.map +1 -1
  10. package/dist/runtime.cjs +435 -33
  11. package/dist/runtime.cjs.map +1 -1
  12. package/dist/runtime.js +434 -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 +11 -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 +31 -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 +4691 -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 +4689 -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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import { z } from 'zod';
3
- import fs2 from 'fs';
3
+ import fs3 from 'fs';
4
4
  import matter from 'gray-matter';
5
5
  import Database from 'better-sqlite3';
6
6
  import remarkMdx from 'remark-mdx';
@@ -131,7 +131,15 @@ function resolveConfig(input, baseDir) {
131
131
  const projectRoot = path.resolve(process.cwd(), raw.rootDir);
132
132
  const contentRoot = path.resolve(projectRoot, raw.contentDir ?? "content");
133
133
  const storePath = path.resolve(projectRoot, raw.store ?? ".scribe/store.sqlite");
134
- const assetsPath = raw.assetsDir ? path.resolve(projectRoot, raw.assetsDir) : void 0;
134
+ let assetsPath;
135
+ let assets;
136
+ if (raw.assets !== void 0 || raw.assetsDir !== void 0) {
137
+ const dir = raw.assets?.dir ?? raw.assetsDir ?? "public";
138
+ const publicPath = raw.assets?.publicPath ?? "/";
139
+ const managedDirs = raw.assets?.managedDirs ?? [];
140
+ assetsPath = path.resolve(projectRoot, dir);
141
+ assets = { assetsPath, publicPath, managedDirs };
142
+ }
135
143
  const seenIds = /* @__PURE__ */ new Set();
136
144
  const types = raw.types.map((type) => {
137
145
  if (seenIds.has(type.id)) {
@@ -146,13 +154,17 @@ function resolveConfig(input, baseDir) {
146
154
  contentDir: type.contentDir ?? type.id,
147
155
  label: type.label ?? type.id.charAt(0).toUpperCase() + type.id.slice(1),
148
156
  slugStrategy: type.slugStrategy ?? "fixed",
149
- indexFallback: type.indexFallback ?? (type.path ? "en" : "none")
157
+ indexFallback: type.indexFallback ?? (type.path ? "en" : "none"),
158
+ // `body` defaults to true (fully backwards compatible). A resolved type
159
+ // always carries an explicit boolean so consumers read one source of truth.
160
+ body: type.body ?? true
150
161
  };
151
162
  });
152
163
  const config = {
153
164
  rootDir: contentRoot,
154
165
  storePath,
155
166
  assetsPath,
167
+ assets,
156
168
  locales: [...raw.locales],
157
169
  defaultLocale,
158
170
  localeRouting,
@@ -188,6 +200,7 @@ function deriveLocaleFallbacks(locales, defaultLocale) {
188
200
  }
189
201
  var FIELD_KIND = /* @__PURE__ */ Symbol.for("@genlook/scribe/fieldKind");
190
202
  var RELATION_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/relationMeta");
203
+ var ASSET_META = /* @__PURE__ */ Symbol.for("@genlook/scribe/assetMeta");
191
204
  function getFieldKind(schema) {
192
205
  const tagged = schema;
193
206
  return tagged[FIELD_KIND] ?? "structural";
@@ -215,6 +228,29 @@ function getRelationTarget(schema) {
215
228
  }
216
229
  return null;
217
230
  }
231
+ function getAssetMeta(schema) {
232
+ let current = schema;
233
+ for (let i = 0; i < 8; i++) {
234
+ const tagged = current;
235
+ if (tagged[ASSET_META]) {
236
+ return tagged[ASSET_META];
237
+ }
238
+ if (typeof tagged.unwrap === "function") {
239
+ current = tagged.unwrap();
240
+ continue;
241
+ }
242
+ if (typeof tagged.removeDefault === "function") {
243
+ current = tagged.removeDefault();
244
+ continue;
245
+ }
246
+ if (tagged._def?.innerType) {
247
+ current = tagged._def.innerType;
248
+ continue;
249
+ }
250
+ break;
251
+ }
252
+ return null;
253
+ }
218
254
  function peelOptionalWrappers(schema) {
219
255
  let current = schema;
220
256
  for (let i = 0; i < 8; i++) {
@@ -248,7 +284,23 @@ function introspectSchema(schema, prefix = []) {
248
284
  kind: "relation",
249
285
  relationTarget: relation.typeId,
250
286
  relationMultiple: relation.multiple,
251
- relationOptional: relation.optional
287
+ relationOptional: relation.optional,
288
+ relationOnTargetDelete: relation.onTargetDelete
289
+ }
290
+ ];
291
+ }
292
+ const asset = getAssetMeta(schema);
293
+ if (asset && prefix.length > 0) {
294
+ return [
295
+ {
296
+ path: prefix,
297
+ kind: "asset",
298
+ assetDir: asset.dir,
299
+ assetTemplate: asset.template,
300
+ assetFormats: asset.formats,
301
+ assetMaxKB: asset.maxKB,
302
+ assetOptional: asset.optional,
303
+ assetOnDelete: asset.onDelete
252
304
  }
253
305
  ];
254
306
  }
@@ -326,7 +378,7 @@ function pickTranslatable(data, schema) {
326
378
  return extractByPaths(data, translatablePaths);
327
379
  }
328
380
  function pickStructural(data, schema) {
329
- const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation").map((f) => f.path);
381
+ const structuralPaths = introspectSchema(schema).filter((f) => f.kind === "structural" || f.kind === "relation" || f.kind === "asset").map((f) => f.path);
330
382
  return extractByPaths(data, structuralPaths);
331
383
  }
332
384
  function mergeStructuralOntoLocale(localeData, enData, schema) {
@@ -341,7 +393,7 @@ function deepMerge(base, overlay) {
341
393
  out[key] = deepMerge(out[key], value);
342
394
  } else if (Array.isArray(value) && Array.isArray(out[key])) {
343
395
  out[key] = mergeArrayOverlay(out[key], value);
344
- } else {
396
+ } else if (value !== void 0) {
345
397
  out[key] = value;
346
398
  }
347
399
  }
@@ -359,6 +411,9 @@ function mergeArrayOverlay(base, overlay) {
359
411
  function listRelationFields(schema) {
360
412
  return introspectSchema(schema).filter((f) => f.kind === "relation");
361
413
  }
414
+ function listAssetFields(schema) {
415
+ return introspectSchema(schema).filter((f) => f.kind === "asset");
416
+ }
362
417
  var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
363
418
  var slugPatternSchema = z.string().regex(SLUG_PATTERN, "slug must be lowercase-kebab-case");
364
419
  var isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/, "Use ISO date YYYY-MM-DD or full ISO 8601");
@@ -439,12 +494,18 @@ function extractBuiltinEnFields(data, _pathTemplate, _enSlug, _defaultLocale) {
439
494
  delete rest[internalKey];
440
495
  }
441
496
  }
497
+ let vars;
498
+ if (rest.vars !== void 0) {
499
+ vars = rest.vars;
500
+ delete rest.vars;
501
+ }
442
502
  return {
443
503
  builtin: {
444
504
  publishedAt,
445
505
  updatedAt,
446
506
  noindex,
447
- canonicalPathOverride
507
+ canonicalPathOverride,
508
+ vars
448
509
  },
449
510
  rest,
450
511
  issues
@@ -474,6 +535,7 @@ function mergeBuiltinsIntoFrontmatter(frontmatter, doc, type, defaultLocale, loc
474
535
  if (doc.updatedAt !== void 0) out.updatedAt = doc.updatedAt;
475
536
  out.noindex = doc.noindex;
476
537
  out.canonicalPath = resolveCanonicalPathname(type, doc, defaultLocale, localeRouting);
538
+ if (doc.vars !== void 0) out.vars = doc.vars;
477
539
  return out;
478
540
  }
479
541
  function seoFieldsFromEn(enDoc) {
@@ -563,7 +625,7 @@ function resolveStorePath(config) {
563
625
  function openStore(config, mode = "readwrite") {
564
626
  const storePath = resolveStorePath(config);
565
627
  if (mode === "readwrite") {
566
- fs2.mkdirSync(path.dirname(storePath), { recursive: true });
628
+ fs3.mkdirSync(path.dirname(storePath), { recursive: true });
567
629
  }
568
630
  const db = new Database(storePath, { readonly: mode === "readonly" });
569
631
  if (mode === "readwrite") {
@@ -602,6 +664,9 @@ function migrate(db) {
602
664
  function listTranslationsForType(db, contentType) {
603
665
  return db.prepare(`SELECT * FROM translations WHERE content_type = ? ORDER BY en_slug, locale`).all(contentType);
604
666
  }
667
+ function bulkLoadTranslations(db) {
668
+ return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
669
+ }
605
670
 
606
671
  // src/translate/normalize-mdx-body.ts
607
672
  function needsMdxEscapeNormalization(body) {
@@ -718,6 +783,279 @@ function prepareTranslatedMdxBody(body) {
718
783
  };
719
784
  }
720
785
 
786
+ // src/loader/resolve-assets.ts
787
+ function joinPublicPath(publicPath, webPath) {
788
+ if (publicPath === "" || publicPath === "/") return webPath;
789
+ const suffix = webPath.startsWith("/") ? webPath : `/${webPath}`;
790
+ return publicPath.replace(/\/+$/, "") + suffix;
791
+ }
792
+ function materializeTemplate(template, enSlug) {
793
+ return template.split("{slug}").join(enSlug);
794
+ }
795
+ function setAssetAtPath(container, path6, transform) {
796
+ const [head, ...rest] = path6;
797
+ if (head === void 0) return;
798
+ if (rest.length === 0) {
799
+ const current = container[head];
800
+ const next = transform(typeof current === "string" ? current : void 0);
801
+ if (next !== void 0) container[head] = next;
802
+ return;
803
+ }
804
+ if (rest[0] === "*") {
805
+ const arr = container[head];
806
+ if (!Array.isArray(arr)) return;
807
+ for (const item of arr) {
808
+ if (item && typeof item === "object" && !Array.isArray(item)) {
809
+ setAssetAtPath(item, rest.slice(1), transform);
810
+ }
811
+ }
812
+ return;
813
+ }
814
+ const child = container[head];
815
+ if (child && typeof child === "object" && !Array.isArray(child)) {
816
+ setAssetAtPath(child, rest, transform);
817
+ }
818
+ }
819
+ function resolveDocumentAssets(doc, assetFields, assets) {
820
+ const frontmatter = doc.frontmatter;
821
+ for (const f of assetFields) {
822
+ setAssetAtPath(frontmatter, f.path, (current) => {
823
+ let value = current;
824
+ if (value === void 0) {
825
+ if (!f.assetTemplate) return void 0;
826
+ value = materializeTemplate(f.assetTemplate, doc.enSlug);
827
+ }
828
+ return joinPublicPath(assets.publicPath, value);
829
+ });
830
+ }
831
+ }
832
+
833
+ // src/inline/tokens.ts
834
+ var TOKEN_OPEN = "${{";
835
+ var ESCAPE_SEQUENCE = "$\\{{";
836
+ var ESCAPE_RESULT = "${{";
837
+ function placeholderMarker(n) {
838
+ return `%%${n}%%`;
839
+ }
840
+ function findTokenEnd(body, start) {
841
+ let i = start;
842
+ let inString = false;
843
+ while (i < body.length) {
844
+ const ch = body[i];
845
+ if (ch === "\n") return -1;
846
+ if (inString) {
847
+ if (ch === "\\") {
848
+ i += 2;
849
+ continue;
850
+ }
851
+ if (ch === '"') inString = false;
852
+ i += 1;
853
+ continue;
854
+ }
855
+ if (ch === '"') {
856
+ inString = true;
857
+ i += 1;
858
+ continue;
859
+ }
860
+ if (ch === "}" && body[i + 1] === "}") return i;
861
+ i += 1;
862
+ }
863
+ return -1;
864
+ }
865
+ function parseTokenInner(inner) {
866
+ const colon = inner.indexOf(":");
867
+ if (colon === -1) {
868
+ return { ok: false, reason: `missing kind separator ":"` };
869
+ }
870
+ const kind = inner.slice(0, colon);
871
+ const rest = inner.slice(colon + 1);
872
+ switch (kind) {
873
+ case "static": {
874
+ let text;
875
+ try {
876
+ text = JSON.parse(rest);
877
+ } catch {
878
+ return { ok: false, reason: `static value is not a valid JSON string` };
879
+ }
880
+ if (typeof text !== "string") {
881
+ return { ok: false, reason: `static value must be a JSON string` };
882
+ }
883
+ return { ok: true, token: { kind: "static", text } };
884
+ }
885
+ case "relation": {
886
+ const parts = rest.split(":");
887
+ if (parts.length < 2 || parts.length > 3) {
888
+ return {
889
+ ok: false,
890
+ reason: `relation must be relation:<typeId>:<enSlug> or relation:<typeId>:<enSlug>:slug`
891
+ };
892
+ }
893
+ const [targetTypeId, enSlug, mode] = parts;
894
+ if (!targetTypeId || !enSlug) {
895
+ return { ok: false, reason: `relation is missing a typeId or enSlug` };
896
+ }
897
+ if (mode !== void 0 && mode !== "slug") {
898
+ return { ok: false, reason: `relation mode must be "slug" (got "${mode}")` };
899
+ }
900
+ return {
901
+ ok: true,
902
+ token: {
903
+ kind: "relation",
904
+ targetTypeId,
905
+ enSlug,
906
+ mode: mode === "slug" ? "slug" : "url"
907
+ }
908
+ };
909
+ }
910
+ case "asset": {
911
+ if (!rest.startsWith("/")) {
912
+ return { ok: false, reason: `asset path must start with "/" (got "${rest}")` };
913
+ }
914
+ return { ok: true, token: { kind: "asset", webPath: rest } };
915
+ }
916
+ case "var": {
917
+ if (!rest) return { ok: false, reason: `var is missing a key` };
918
+ return { ok: true, token: { kind: "var", key: rest } };
919
+ }
920
+ default:
921
+ return { ok: false, reason: `unknown token kind "${kind}"` };
922
+ }
923
+ }
924
+ function extractInlineTokens(body) {
925
+ const tokens = [];
926
+ const malformed = [];
927
+ let out = "";
928
+ let i = 0;
929
+ while (i < body.length) {
930
+ const open = body.indexOf(TOKEN_OPEN, i);
931
+ if (open === -1) {
932
+ out += body.slice(i);
933
+ break;
934
+ }
935
+ out += body.slice(i, open);
936
+ const contentStart = open + TOKEN_OPEN.length;
937
+ const end = findTokenEnd(body, contentStart);
938
+ if (end === -1) {
939
+ malformed.push({
940
+ raw: body.slice(open),
941
+ reason: `unterminated token (missing "}}")`,
942
+ index: open
943
+ });
944
+ out += body.slice(open);
945
+ break;
946
+ }
947
+ const raw = body.slice(open, end + 2);
948
+ const inner = body.slice(contentStart, end);
949
+ const parsed = parseTokenInner(inner);
950
+ if (parsed.ok) {
951
+ tokens.push({ ...parsed.token, raw });
952
+ out += placeholderMarker(tokens.length);
953
+ } else {
954
+ malformed.push({ raw, reason: parsed.reason, index: open });
955
+ out += raw;
956
+ }
957
+ i = end + 2;
958
+ }
959
+ return { placeholderBody: out, tokens, malformed };
960
+ }
961
+ function fillPlaceholders(body, resolvedValues) {
962
+ return body.replace(/%%(\d+)%%/g, (match, n) => {
963
+ const idx = Number(n) - 1;
964
+ return idx >= 0 && idx < resolvedValues.length ? resolvedValues[idx] : match;
965
+ });
966
+ }
967
+ function unescapeInlineTokens(body) {
968
+ return body.split(ESCAPE_SEQUENCE).join(ESCAPE_RESULT);
969
+ }
970
+
971
+ // src/inline/resolve-tokens.ts
972
+ function slugKey(typeId, enSlug, locale) {
973
+ return `${typeId}\0${enSlug}\0${locale}`;
974
+ }
975
+ function isStringRecord(value) {
976
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
977
+ return Object.values(value).every((v) => typeof v === "string");
978
+ }
979
+ function createInlineResolver(config) {
980
+ const urlBuilder = createUrlBuilder(config);
981
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
982
+ const storePath = resolveStorePath(config);
983
+ let slugCache = null;
984
+ const localizedSlugIndex = () => {
985
+ let mtime = 0;
986
+ try {
987
+ mtime = fs3.statSync(storePath).mtimeMs;
988
+ } catch {
989
+ mtime = 0;
990
+ }
991
+ if (slugCache && slugCache.mtime === mtime) return slugCache.index;
992
+ const index = /* @__PURE__ */ new Map();
993
+ try {
994
+ const db = openStore(config, "readonly");
995
+ try {
996
+ for (const row of bulkLoadTranslations(db)) {
997
+ index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
998
+ }
999
+ } finally {
1000
+ db.close();
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ slugCache = { mtime, index };
1005
+ return index;
1006
+ };
1007
+ const localizedSlug = (typeId, enSlug, locale) => {
1008
+ if (locale === config.defaultLocale) return enSlug;
1009
+ const index = localizedSlugIndex();
1010
+ const direct = index.get(slugKey(typeId, enSlug, locale));
1011
+ if (direct) return direct;
1012
+ for (const fb of config.localeFallbacks?.[locale] ?? []) {
1013
+ const hit = index.get(slugKey(typeId, enSlug, fb));
1014
+ if (hit) return hit;
1015
+ }
1016
+ return enSlug;
1017
+ };
1018
+ return {
1019
+ resolve(token, enFrontmatter, locale) {
1020
+ switch (token.kind) {
1021
+ case "static":
1022
+ return token.text;
1023
+ case "var": {
1024
+ const vars = enFrontmatter.vars;
1025
+ if (isStringRecord(vars) && typeof vars[token.key] === "string") {
1026
+ return vars[token.key];
1027
+ }
1028
+ return "";
1029
+ }
1030
+ case "asset": {
1031
+ const assets = config.assets;
1032
+ return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
1033
+ }
1034
+ case "relation": {
1035
+ const type = typeById.get(token.targetTypeId);
1036
+ if (!type) return "";
1037
+ if (token.mode === "slug") return token.enSlug;
1038
+ if (!isRoutableType(type)) return "";
1039
+ const slug = localizedSlug(token.targetTypeId, token.enSlug, locale);
1040
+ return urlBuilder.resolvePath(type.path, slug, locale);
1041
+ }
1042
+ }
1043
+ }
1044
+ };
1045
+ }
1046
+ function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
1047
+ const { placeholderBody, tokens } = extractInlineTokens(rawBody);
1048
+ if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
1049
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
1050
+ return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
1051
+ }
1052
+ function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
1053
+ const { tokens } = extractInlineTokens(enRawBody);
1054
+ if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
1055
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
1056
+ return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
1057
+ }
1058
+
721
1059
  // src/loader/normalize-en.ts
722
1060
  function normalizeEnFrontmatter(data) {
723
1061
  const out = { ...data };
@@ -743,14 +1081,14 @@ function isPostFile(name) {
743
1081
  return isPublishableContentFile(name);
744
1082
  }
745
1083
  function listEnFiles(contentDir) {
746
- if (!fs2.existsSync(contentDir)) return [];
747
- return fs2.readdirSync(contentDir).filter(isPostFile).map((f) => path.join(contentDir, f));
1084
+ if (!fs3.existsSync(contentDir)) return [];
1085
+ return fs3.readdirSync(contentDir).filter(isPostFile).map((f) => path.join(contentDir, f));
748
1086
  }
749
1087
  function slugFromPath(filePath) {
750
1088
  return path.basename(filePath).replace(/\.(md|mdx)$/, "");
751
1089
  }
752
1090
  function parseEnMdx(filePath, config, type) {
753
- const raw = fs2.readFileSync(filePath, "utf8");
1091
+ const raw = fs3.readFileSync(filePath, "utf8");
754
1092
  const parsed = matter(raw);
755
1093
  const slug = slugFromPath(filePath);
756
1094
  const normalized = normalizeEnFrontmatter(parsed.data);
@@ -789,6 +1127,7 @@ function parseEnMdx(filePath, config, type) {
789
1127
  updatedAt: builtin.updatedAt,
790
1128
  noindex: builtin.noindex,
791
1129
  canonicalPathOverride: builtin.canonicalPathOverride,
1130
+ vars: builtin.vars,
792
1131
  slug,
793
1132
  locale: config.defaultLocale
794
1133
  },
@@ -813,12 +1152,15 @@ function parseEnMdx(filePath, config, type) {
813
1152
  noindex: builtin.noindex,
814
1153
  canonicalPathOverride: builtin.canonicalPathOverride,
815
1154
  frontmatter,
816
- content: parsed.content,
1155
+ // Bodyless types (`body: false`) never carry a body: the loader skips the
1156
+ // MDX body entirely so runtimes, exports, and hashing all see an empty body.
1157
+ // A stray body is reported separately by `scribe validate`.
1158
+ content: type.body === false ? "" : parsed.content,
817
1159
  filePath
818
1160
  };
819
1161
  return { document, issues };
820
1162
  }
821
- function buildDocumentFromTranslation(row, enDoc, type, config) {
1163
+ function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
822
1164
  const localeFm = JSON.parse(row.frontmatter_json);
823
1165
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
824
1166
  const seo = seoFieldsFromEn(enDoc);
@@ -829,6 +1171,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
829
1171
  config.defaultLocale,
830
1172
  config.localeRouting
831
1173
  );
1174
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1175
+ const content = inlineResolver ? fillTranslatedInlineBody(
1176
+ preparedBody,
1177
+ enDoc.content,
1178
+ enDoc.frontmatter,
1179
+ row.locale,
1180
+ inlineResolver
1181
+ ) : preparedBody;
832
1182
  return {
833
1183
  slug: row.slug,
834
1184
  enSlug: row.en_slug,
@@ -838,14 +1188,16 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
838
1188
  noindex: seo.noindex,
839
1189
  canonicalPathOverride: seo.canonicalPathOverride,
840
1190
  frontmatter,
841
- content: prepareTranslatedMdxBody(row.body).body
1191
+ content
842
1192
  };
843
1193
  }
844
1194
  var DEV_REVALIDATE_MS = 1500;
845
- function createContentLoader(config, type) {
1195
+ var contentVersion = 0;
1196
+ function createContentLoader(config, type, options = {}) {
846
1197
  let cached = null;
847
1198
  let signature = "";
848
1199
  let lastCheck = 0;
1200
+ let builtVersion = contentVersion;
849
1201
  const contentDir = path.join(
850
1202
  /* turbopackIgnore: true */
851
1203
  config.rootDir,
@@ -853,18 +1205,34 @@ function createContentLoader(config, type) {
853
1205
  );
854
1206
  const storePath = resolveStorePath(config);
855
1207
  const isProd = process.env.NODE_ENV === "production";
1208
+ const assets = config.assets;
1209
+ const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
1210
+ const resolveAssets = (doc) => {
1211
+ if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
1212
+ };
1213
+ const inlineResolver = options.inlineResolver;
1214
+ const resolveEnInline = (doc) => {
1215
+ if (inlineResolver) {
1216
+ doc.content = substituteEnInlineBody(
1217
+ doc.content,
1218
+ doc.frontmatter,
1219
+ config.defaultLocale,
1220
+ inlineResolver
1221
+ );
1222
+ }
1223
+ };
856
1224
  function computeSignature() {
857
1225
  const files = listEnFiles(contentDir);
858
1226
  let newest = 0;
859
1227
  for (const f of files) {
860
1228
  try {
861
- newest = Math.max(newest, fs2.statSync(f).mtimeMs);
1229
+ newest = Math.max(newest, fs3.statSync(f).mtimeMs);
862
1230
  } catch {
863
1231
  }
864
1232
  }
865
1233
  let store = 0;
866
1234
  try {
867
- store = fs2.statSync(storePath).mtimeMs;
1235
+ store = fs3.statSync(storePath).mtimeMs;
868
1236
  } catch {
869
1237
  }
870
1238
  return `${files.length}:${newest}:${store}`;
@@ -910,16 +1278,28 @@ function createContentLoader(config, type) {
910
1278
  for (const row of rowsByLocale.get(locale) ?? []) {
911
1279
  const enDoc = englishBySlug.get(row.en_slug);
912
1280
  if (!enDoc) continue;
913
- const doc = buildDocumentFromTranslation(row, enDoc, type, config);
1281
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
1282
+ resolveAssets(doc);
914
1283
  bySlug.set(doc.slug, doc);
915
1284
  byEnSlug.set(row.en_slug, doc);
916
1285
  }
917
1286
  out.set(locale, { bySlug, byEnSlug });
918
1287
  }
1288
+ for (const doc of englishBySlug.values()) {
1289
+ resolveAssets(doc);
1290
+ resolveEnInline(doc);
1291
+ }
919
1292
  return out;
920
1293
  }
921
1294
  return () => {
922
1295
  if (cached) {
1296
+ if (contentVersion !== builtVersion) {
1297
+ cached = build();
1298
+ builtVersion = contentVersion;
1299
+ lastCheck = Date.now();
1300
+ signature = isProd ? "" : computeSignature();
1301
+ return cached;
1302
+ }
923
1303
  if (isProd) return cached;
924
1304
  const now = Date.now();
925
1305
  if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
@@ -932,6 +1312,7 @@ function createContentLoader(config, type) {
932
1312
  }
933
1313
  lastCheck = Date.now();
934
1314
  cached = build();
1315
+ builtVersion = contentVersion;
935
1316
  signature = isProd ? "" : computeSignature();
936
1317
  return cached;
937
1318
  };
@@ -1021,8 +1402,11 @@ function comparatorFor(orderBy) {
1021
1402
  return (a, b) => a.slug.localeCompare(b.slug);
1022
1403
  }
1023
1404
  }
1024
- function buildRuntime(config, type, getRuntime) {
1025
- const load = createContentLoader(config, type);
1405
+ function buildRuntime(config, type, getRuntime, options = {}) {
1406
+ const load = createContentLoader(config, type, {
1407
+ resolveAssets: options.resolveAssets,
1408
+ inlineResolver: options.inlineResolver
1409
+ });
1026
1410
  const relationFields = new Map(
1027
1411
  listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1028
1412
  );
@@ -1039,12 +1423,12 @@ function buildRuntime(config, type, getRuntime) {
1039
1423
  id: type.id,
1040
1424
  config: type,
1041
1425
  load,
1042
- list(locale = config.defaultLocale, options = {}) {
1426
+ list(locale = config.defaultLocale, options2 = {}) {
1043
1427
  const idx = load().get(locale);
1044
1428
  if (!idx) return [];
1045
1429
  const docs = Array.from(idx.bySlug.values());
1046
- docs.sort(comparatorFor(options.orderBy ?? type.orderBy ?? "slug"));
1047
- return options.limit !== void 0 ? docs.slice(0, options.limit) : docs;
1430
+ docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
1431
+ return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
1048
1432
  },
1049
1433
  get(slug, locale = config.defaultLocale) {
1050
1434
  return load().get(locale)?.bySlug.get(slug) ?? null;
@@ -1057,7 +1441,7 @@ function buildRuntime(config, type, getRuntime) {
1057
1441
  load(),
1058
1442
  type,
1059
1443
  config.localeRouting,
1060
- config.localeFallbacks[locale] ?? []
1444
+ config.localeFallbacks?.[locale] ?? []
1061
1445
  );
1062
1446
  if (result.document && type.path) {
1063
1447
  return {
@@ -1071,15 +1455,15 @@ function buildRuntime(config, type, getRuntime) {
1071
1455
  }
1072
1456
  return result;
1073
1457
  },
1074
- staticParams(options = {}) {
1458
+ staticParams(options2 = {}) {
1075
1459
  assertRoutable("staticParams");
1076
1460
  const all = load();
1077
1461
  const enIdx = all.get(config.defaultLocale);
1078
1462
  if (!enIdx) return [];
1079
1463
  const params = [];
1080
- for (const locale of options.locales ?? config.locales) {
1464
+ for (const locale of options2.locales ?? config.locales) {
1081
1465
  const localeIdx = all.get(locale);
1082
- const fallbacks = config.localeFallbacks[locale] ?? [];
1466
+ const fallbacks = config.localeFallbacks?.[locale] ?? [];
1083
1467
  for (const doc of enIdx.bySlug.values()) {
1084
1468
  let slug;
1085
1469
  if (locale === config.defaultLocale) {
@@ -1158,7 +1542,7 @@ function buildRuntime(config, type, getRuntime) {
1158
1542
  };
1159
1543
  return runtime;
1160
1544
  }
1161
- function createProject(config) {
1545
+ function createProject(config, options = {}) {
1162
1546
  const runtimes = /* @__PURE__ */ new Map();
1163
1547
  const getRuntime = (id) => {
1164
1548
  const runtime = runtimes.get(id);
@@ -1167,8 +1551,15 @@ function createProject(config) {
1167
1551
  }
1168
1552
  return runtime;
1169
1553
  };
1554
+ const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config) : void 0;
1170
1555
  for (const type of config.types) {
1171
- runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1556
+ runtimes.set(
1557
+ type.id,
1558
+ buildRuntime(config, type, getRuntime, {
1559
+ resolveAssets: options.resolveAssets,
1560
+ inlineResolver
1561
+ })
1562
+ );
1172
1563
  }
1173
1564
  return {
1174
1565
  config,
@@ -1261,10 +1652,10 @@ function redirectsFilePath(config, type) {
1261
1652
  }
1262
1653
  function loadTypeRedirectsFile(config, type) {
1263
1654
  const filePath = redirectsFilePath(config, type);
1264
- if (!fs2.existsSync(filePath)) return null;
1655
+ if (!fs3.existsSync(filePath)) return null;
1265
1656
  let raw;
1266
1657
  try {
1267
- raw = JSON.parse(fs2.readFileSync(filePath, "utf8"));
1658
+ raw = JSON.parse(fs3.readFileSync(filePath, "utf8"));
1268
1659
  } catch (error) {
1269
1660
  throw new Error(
1270
1661
  `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
@@ -1420,7 +1811,7 @@ async function generateSitemap(project, options) {
1420
1811
  // src/create-scribe.ts
1421
1812
  function createScribe(input) {
1422
1813
  const config = resolveConfig(input);
1423
- const project = createProject(config);
1814
+ const project = createProject(config, { resolveAssets: true, resolveInlineTokens: true });
1424
1815
  const scribe = {
1425
1816
  config,
1426
1817
  project,
@@ -1429,6 +1820,17 @@ function createScribe(input) {
1429
1820
  listRoutableTypes: project.listRoutableTypes,
1430
1821
  sitemap(options) {
1431
1822
  return generateSitemap(project, options);
1823
+ },
1824
+ assets: {
1825
+ url(ref, opts) {
1826
+ if (opts && Object.keys(opts).length > 0) {
1827
+ throw new Error(
1828
+ `scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
1829
+ );
1830
+ }
1831
+ const assetsConfig = config.assets;
1832
+ return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
1833
+ }
1432
1834
  }
1433
1835
  };
1434
1836
  for (const type of config.types) {