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.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,284 @@ 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 !== 3) {
888
+ return {
889
+ ok: false,
890
+ reason: `relation must be relation:<typeId>:<enSlug>:href 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 !== "href" && mode !== "slug") {
898
+ return { ok: false, reason: `relation mode must be "href" or "slug" (got "${mode}")` };
899
+ }
900
+ return {
901
+ ok: true,
902
+ token: {
903
+ kind: "relation",
904
+ targetTypeId,
905
+ enSlug,
906
+ mode
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, options = {}) {
980
+ const linkStyle = options.linkStyle ?? "app";
981
+ const exportExtension = options.exportExtension ?? ".md";
982
+ const urlBuilder = createUrlBuilder(config);
983
+ const typeById = new Map(config.types.map((t) => [t.id, t]));
984
+ const storePath = resolveStorePath(config);
985
+ let slugCache = null;
986
+ const localizedSlugIndex = () => {
987
+ let mtime = 0;
988
+ try {
989
+ mtime = fs3.statSync(storePath).mtimeMs;
990
+ } catch {
991
+ mtime = 0;
992
+ }
993
+ if (slugCache && slugCache.mtime === mtime) return slugCache.index;
994
+ const index = /* @__PURE__ */ new Map();
995
+ try {
996
+ const db = openStore(config, "readonly");
997
+ try {
998
+ for (const row of bulkLoadTranslations(db)) {
999
+ index.set(slugKey(row.content_type, row.en_slug, row.locale), row.slug);
1000
+ }
1001
+ } finally {
1002
+ db.close();
1003
+ }
1004
+ } catch {
1005
+ }
1006
+ slugCache = { mtime, index };
1007
+ return index;
1008
+ };
1009
+ const localizedSlug = (typeId, enSlug, locale) => {
1010
+ if (locale === config.defaultLocale) return enSlug;
1011
+ const index = localizedSlugIndex();
1012
+ const direct = index.get(slugKey(typeId, enSlug, locale));
1013
+ if (direct) return direct;
1014
+ for (const fb of config.localeFallbacks?.[locale] ?? []) {
1015
+ const hit = index.get(slugKey(typeId, enSlug, fb));
1016
+ if (hit) return hit;
1017
+ }
1018
+ return enSlug;
1019
+ };
1020
+ return {
1021
+ resolve(token, enFrontmatter, locale) {
1022
+ switch (token.kind) {
1023
+ case "static":
1024
+ return token.text;
1025
+ case "var": {
1026
+ const vars = enFrontmatter.vars;
1027
+ if (isStringRecord(vars) && typeof vars[token.key] === "string") {
1028
+ return vars[token.key];
1029
+ }
1030
+ return "";
1031
+ }
1032
+ case "asset": {
1033
+ const assets = config.assets;
1034
+ return assets ? joinPublicPath(assets.publicPath, token.webPath) : token.webPath;
1035
+ }
1036
+ case "relation": {
1037
+ const type = typeById.get(token.targetTypeId);
1038
+ if (!type) return "";
1039
+ if (token.mode === "slug") return token.enSlug;
1040
+ if (!isRoutableType(type)) return "";
1041
+ const slug = localizedSlug(token.targetTypeId, token.enSlug, locale);
1042
+ if (linkStyle === "export") {
1043
+ return urlBuilder.resolvePath(type.path, `${slug}${exportExtension}`, locale);
1044
+ }
1045
+ return type.path.replace("{slug}", slug);
1046
+ }
1047
+ }
1048
+ }
1049
+ };
1050
+ }
1051
+ function substituteEnInlineBody(rawBody, enFrontmatter, defaultLocale, resolver) {
1052
+ const { placeholderBody, tokens } = extractInlineTokens(rawBody);
1053
+ if (tokens.length === 0) return unescapeInlineTokens(placeholderBody);
1054
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, defaultLocale));
1055
+ return unescapeInlineTokens(fillPlaceholders(placeholderBody, values));
1056
+ }
1057
+ function fillTranslatedInlineBody(translatedBody, enRawBody, enFrontmatter, locale, resolver) {
1058
+ const { tokens } = extractInlineTokens(enRawBody);
1059
+ if (tokens.length === 0) return unescapeInlineTokens(translatedBody);
1060
+ const values = tokens.map((t) => resolver.resolve(t, enFrontmatter, locale));
1061
+ return unescapeInlineTokens(fillPlaceholders(translatedBody, values));
1062
+ }
1063
+
721
1064
  // src/loader/normalize-en.ts
722
1065
  function normalizeEnFrontmatter(data) {
723
1066
  const out = { ...data };
@@ -743,14 +1086,14 @@ function isPostFile(name) {
743
1086
  return isPublishableContentFile(name);
744
1087
  }
745
1088
  function listEnFiles(contentDir) {
746
- if (!fs2.existsSync(contentDir)) return [];
747
- return fs2.readdirSync(contentDir).filter(isPostFile).map((f) => path.join(contentDir, f));
1089
+ if (!fs3.existsSync(contentDir)) return [];
1090
+ return fs3.readdirSync(contentDir).filter(isPostFile).map((f) => path.join(contentDir, f));
748
1091
  }
749
1092
  function slugFromPath(filePath) {
750
1093
  return path.basename(filePath).replace(/\.(md|mdx)$/, "");
751
1094
  }
752
1095
  function parseEnMdx(filePath, config, type) {
753
- const raw = fs2.readFileSync(filePath, "utf8");
1096
+ const raw = fs3.readFileSync(filePath, "utf8");
754
1097
  const parsed = matter(raw);
755
1098
  const slug = slugFromPath(filePath);
756
1099
  const normalized = normalizeEnFrontmatter(parsed.data);
@@ -789,6 +1132,7 @@ function parseEnMdx(filePath, config, type) {
789
1132
  updatedAt: builtin.updatedAt,
790
1133
  noindex: builtin.noindex,
791
1134
  canonicalPathOverride: builtin.canonicalPathOverride,
1135
+ vars: builtin.vars,
792
1136
  slug,
793
1137
  locale: config.defaultLocale
794
1138
  },
@@ -813,12 +1157,15 @@ function parseEnMdx(filePath, config, type) {
813
1157
  noindex: builtin.noindex,
814
1158
  canonicalPathOverride: builtin.canonicalPathOverride,
815
1159
  frontmatter,
816
- content: parsed.content,
1160
+ // Bodyless types (`body: false`) never carry a body: the loader skips the
1161
+ // MDX body entirely so runtimes, exports, and hashing all see an empty body.
1162
+ // A stray body is reported separately by `scribe validate`.
1163
+ content: type.body === false ? "" : parsed.content,
817
1164
  filePath
818
1165
  };
819
1166
  return { document, issues };
820
1167
  }
821
- function buildDocumentFromTranslation(row, enDoc, type, config) {
1168
+ function buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver) {
822
1169
  const localeFm = JSON.parse(row.frontmatter_json);
823
1170
  const merged = mergeStructuralOntoLocale(localeFm, enDoc.frontmatter, type.schema);
824
1171
  const seo = seoFieldsFromEn(enDoc);
@@ -829,6 +1176,14 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
829
1176
  config.defaultLocale,
830
1177
  config.localeRouting
831
1178
  );
1179
+ const preparedBody = prepareTranslatedMdxBody(row.body).body;
1180
+ const content = inlineResolver ? fillTranslatedInlineBody(
1181
+ preparedBody,
1182
+ enDoc.content,
1183
+ enDoc.frontmatter,
1184
+ row.locale,
1185
+ inlineResolver
1186
+ ) : preparedBody;
832
1187
  return {
833
1188
  slug: row.slug,
834
1189
  enSlug: row.en_slug,
@@ -838,14 +1193,16 @@ function buildDocumentFromTranslation(row, enDoc, type, config) {
838
1193
  noindex: seo.noindex,
839
1194
  canonicalPathOverride: seo.canonicalPathOverride,
840
1195
  frontmatter,
841
- content: prepareTranslatedMdxBody(row.body).body
1196
+ content
842
1197
  };
843
1198
  }
844
1199
  var DEV_REVALIDATE_MS = 1500;
845
- function createContentLoader(config, type) {
1200
+ var contentVersion = 0;
1201
+ function createContentLoader(config, type, options = {}) {
846
1202
  let cached = null;
847
1203
  let signature = "";
848
1204
  let lastCheck = 0;
1205
+ let builtVersion = contentVersion;
849
1206
  const contentDir = path.join(
850
1207
  /* turbopackIgnore: true */
851
1208
  config.rootDir,
@@ -853,18 +1210,34 @@ function createContentLoader(config, type) {
853
1210
  );
854
1211
  const storePath = resolveStorePath(config);
855
1212
  const isProd = process.env.NODE_ENV === "production";
1213
+ const assets = config.assets;
1214
+ const assetFields = assets && options.resolveAssets ? listAssetFields(type.schema) : [];
1215
+ const resolveAssets = (doc) => {
1216
+ if (assetFields.length > 0 && assets) resolveDocumentAssets(doc, assetFields, assets);
1217
+ };
1218
+ const inlineResolver = options.inlineResolver;
1219
+ const resolveEnInline = (doc) => {
1220
+ if (inlineResolver) {
1221
+ doc.content = substituteEnInlineBody(
1222
+ doc.content,
1223
+ doc.frontmatter,
1224
+ config.defaultLocale,
1225
+ inlineResolver
1226
+ );
1227
+ }
1228
+ };
856
1229
  function computeSignature() {
857
1230
  const files = listEnFiles(contentDir);
858
1231
  let newest = 0;
859
1232
  for (const f of files) {
860
1233
  try {
861
- newest = Math.max(newest, fs2.statSync(f).mtimeMs);
1234
+ newest = Math.max(newest, fs3.statSync(f).mtimeMs);
862
1235
  } catch {
863
1236
  }
864
1237
  }
865
1238
  let store = 0;
866
1239
  try {
867
- store = fs2.statSync(storePath).mtimeMs;
1240
+ store = fs3.statSync(storePath).mtimeMs;
868
1241
  } catch {
869
1242
  }
870
1243
  return `${files.length}:${newest}:${store}`;
@@ -910,16 +1283,28 @@ function createContentLoader(config, type) {
910
1283
  for (const row of rowsByLocale.get(locale) ?? []) {
911
1284
  const enDoc = englishBySlug.get(row.en_slug);
912
1285
  if (!enDoc) continue;
913
- const doc = buildDocumentFromTranslation(row, enDoc, type, config);
1286
+ const doc = buildDocumentFromTranslation(row, enDoc, type, config, inlineResolver);
1287
+ resolveAssets(doc);
914
1288
  bySlug.set(doc.slug, doc);
915
1289
  byEnSlug.set(row.en_slug, doc);
916
1290
  }
917
1291
  out.set(locale, { bySlug, byEnSlug });
918
1292
  }
1293
+ for (const doc of englishBySlug.values()) {
1294
+ resolveAssets(doc);
1295
+ resolveEnInline(doc);
1296
+ }
919
1297
  return out;
920
1298
  }
921
1299
  return () => {
922
1300
  if (cached) {
1301
+ if (contentVersion !== builtVersion) {
1302
+ cached = build();
1303
+ builtVersion = contentVersion;
1304
+ lastCheck = Date.now();
1305
+ signature = isProd ? "" : computeSignature();
1306
+ return cached;
1307
+ }
923
1308
  if (isProd) return cached;
924
1309
  const now = Date.now();
925
1310
  if (now - lastCheck < DEV_REVALIDATE_MS) return cached;
@@ -932,6 +1317,7 @@ function createContentLoader(config, type) {
932
1317
  }
933
1318
  lastCheck = Date.now();
934
1319
  cached = build();
1320
+ builtVersion = contentVersion;
935
1321
  signature = isProd ? "" : computeSignature();
936
1322
  return cached;
937
1323
  };
@@ -1021,8 +1407,11 @@ function comparatorFor(orderBy) {
1021
1407
  return (a, b) => a.slug.localeCompare(b.slug);
1022
1408
  }
1023
1409
  }
1024
- function buildRuntime(config, type, getRuntime) {
1025
- const load = createContentLoader(config, type);
1410
+ function buildRuntime(config, type, getRuntime, options = {}) {
1411
+ const load = createContentLoader(config, type, {
1412
+ resolveAssets: options.resolveAssets,
1413
+ inlineResolver: options.inlineResolver
1414
+ });
1026
1415
  const relationFields = new Map(
1027
1416
  listRelationFields(type.schema).filter((f) => f.path.length === 1).map((f) => [f.path[0], f])
1028
1417
  );
@@ -1039,12 +1428,12 @@ function buildRuntime(config, type, getRuntime) {
1039
1428
  id: type.id,
1040
1429
  config: type,
1041
1430
  load,
1042
- list(locale = config.defaultLocale, options = {}) {
1431
+ list(locale = config.defaultLocale, options2 = {}) {
1043
1432
  const idx = load().get(locale);
1044
1433
  if (!idx) return [];
1045
1434
  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;
1435
+ docs.sort(comparatorFor(options2.orderBy ?? type.orderBy ?? "slug"));
1436
+ return options2.limit !== void 0 ? docs.slice(0, options2.limit) : docs;
1048
1437
  },
1049
1438
  get(slug, locale = config.defaultLocale) {
1050
1439
  return load().get(locale)?.bySlug.get(slug) ?? null;
@@ -1057,7 +1446,7 @@ function buildRuntime(config, type, getRuntime) {
1057
1446
  load(),
1058
1447
  type,
1059
1448
  config.localeRouting,
1060
- config.localeFallbacks[locale] ?? []
1449
+ config.localeFallbacks?.[locale] ?? []
1061
1450
  );
1062
1451
  if (result.document && type.path) {
1063
1452
  return {
@@ -1071,15 +1460,15 @@ function buildRuntime(config, type, getRuntime) {
1071
1460
  }
1072
1461
  return result;
1073
1462
  },
1074
- staticParams(options = {}) {
1463
+ staticParams(options2 = {}) {
1075
1464
  assertRoutable("staticParams");
1076
1465
  const all = load();
1077
1466
  const enIdx = all.get(config.defaultLocale);
1078
1467
  if (!enIdx) return [];
1079
1468
  const params = [];
1080
- for (const locale of options.locales ?? config.locales) {
1469
+ for (const locale of options2.locales ?? config.locales) {
1081
1470
  const localeIdx = all.get(locale);
1082
- const fallbacks = config.localeFallbacks[locale] ?? [];
1471
+ const fallbacks = config.localeFallbacks?.[locale] ?? [];
1083
1472
  for (const doc of enIdx.bySlug.values()) {
1084
1473
  let slug;
1085
1474
  if (locale === config.defaultLocale) {
@@ -1158,7 +1547,7 @@ function buildRuntime(config, type, getRuntime) {
1158
1547
  };
1159
1548
  return runtime;
1160
1549
  }
1161
- function createProject(config) {
1550
+ function createProject(config, options = {}) {
1162
1551
  const runtimes = /* @__PURE__ */ new Map();
1163
1552
  const getRuntime = (id) => {
1164
1553
  const runtime = runtimes.get(id);
@@ -1167,8 +1556,18 @@ function createProject(config) {
1167
1556
  }
1168
1557
  return runtime;
1169
1558
  };
1559
+ const inlineResolver = options.resolveInlineTokens ? createInlineResolver(config, {
1560
+ linkStyle: options.inlineLinkStyle,
1561
+ exportExtension: options.exportLinkExtension
1562
+ }) : void 0;
1170
1563
  for (const type of config.types) {
1171
- runtimes.set(type.id, buildRuntime(config, type, getRuntime));
1564
+ runtimes.set(
1565
+ type.id,
1566
+ buildRuntime(config, type, getRuntime, {
1567
+ resolveAssets: options.resolveAssets,
1568
+ inlineResolver
1569
+ })
1570
+ );
1172
1571
  }
1173
1572
  return {
1174
1573
  config,
@@ -1261,10 +1660,10 @@ function redirectsFilePath(config, type) {
1261
1660
  }
1262
1661
  function loadTypeRedirectsFile(config, type) {
1263
1662
  const filePath = redirectsFilePath(config, type);
1264
- if (!fs2.existsSync(filePath)) return null;
1663
+ if (!fs3.existsSync(filePath)) return null;
1265
1664
  let raw;
1266
1665
  try {
1267
- raw = JSON.parse(fs2.readFileSync(filePath, "utf8"));
1666
+ raw = JSON.parse(fs3.readFileSync(filePath, "utf8"));
1268
1667
  } catch (error) {
1269
1668
  throw new Error(
1270
1669
  `${type.id}: failed to parse ${TYPE_REDIRECTS_FILENAME}: ${error instanceof Error ? error.message : String(error)}`
@@ -1420,7 +1819,11 @@ async function generateSitemap(project, options) {
1420
1819
  // src/create-scribe.ts
1421
1820
  function createScribe(input) {
1422
1821
  const config = resolveConfig(input);
1423
- const project = createProject(config);
1822
+ const project = createProject(config, {
1823
+ resolveAssets: true,
1824
+ resolveInlineTokens: true,
1825
+ inlineLinkStyle: "app"
1826
+ });
1424
1827
  const scribe = {
1425
1828
  config,
1426
1829
  project,
@@ -1429,6 +1832,17 @@ function createScribe(input) {
1429
1832
  listRoutableTypes: project.listRoutableTypes,
1430
1833
  sitemap(options) {
1431
1834
  return generateSitemap(project, options);
1835
+ },
1836
+ assets: {
1837
+ url(ref, opts) {
1838
+ if (opts && Object.keys(opts).length > 0) {
1839
+ throw new Error(
1840
+ `scribe.assets.url: options are reserved for a future pipeline; got ${Object.keys(opts).join(", ")}`
1841
+ );
1842
+ }
1843
+ const assetsConfig = config.assets;
1844
+ return assetsConfig ? joinPublicPath(assetsConfig.publicPath, ref) : ref;
1845
+ }
1432
1846
  }
1433
1847
  };
1434
1848
  for (const type of config.types) {