@notionx/core 0.1.1 → 0.1.3

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/dist/auth/index.d.ts +1 -1
  2. package/dist/auth/index.js.map +1 -1
  3. package/dist/auth/rate-limit.js.map +1 -1
  4. package/dist/auth/routes/google-callback.js.map +1 -1
  5. package/dist/auth/routes/google.js.map +1 -1
  6. package/dist/auth/routes/index.js.map +1 -1
  7. package/dist/auth/routes/verify-email.js.map +1 -1
  8. package/dist/auth/routes/viewer.js.map +1 -1
  9. package/dist/auth/turnstile.js.map +1 -1
  10. package/dist/auth/user-session.d.ts +1 -1
  11. package/dist/auth/user-session.js.map +1 -1
  12. package/dist/auth/users.js.map +1 -1
  13. package/dist/content/index.d.ts +3 -2
  14. package/dist/content/index.js +176 -60
  15. package/dist/content/index.js.map +1 -1
  16. package/dist/content/localized.d.ts +67 -0
  17. package/dist/content/localized.js +170 -0
  18. package/dist/content/localized.js.map +1 -0
  19. package/dist/content/revalidate.d.ts +2 -1
  20. package/dist/content/revalidate.js +5 -28
  21. package/dist/content/revalidate.js.map +1 -1
  22. package/dist/content/search-index.d.ts +1 -1
  23. package/dist/content/search-index.js.map +1 -1
  24. package/dist/content/search.d.ts +2 -5
  25. package/dist/content/search.js +3 -32
  26. package/dist/content/search.js.map +1 -1
  27. package/dist/email/index.js.map +1 -1
  28. package/dist/{env-C5qu-0R-.d.ts → env-hoez1e-n.d.ts} +0 -4
  29. package/dist/i18n/index.d.ts +18 -24
  30. package/dist/i18n/index.js +29 -54
  31. package/dist/i18n/index.js.map +1 -1
  32. package/dist/index.js +0 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/internal/admin/index.js.map +1 -1
  35. package/dist/media/index.js +3 -2
  36. package/dist/media/index.js.map +1 -1
  37. package/dist/media/routes/index.js +0 -1
  38. package/dist/media/routes/index.js.map +1 -1
  39. package/dist/media/routes/notion-media.js +0 -1
  40. package/dist/media/routes/notion-media.js.map +1 -1
  41. package/dist/notion/config.d.ts +1 -4
  42. package/dist/notion/config.js +1 -23
  43. package/dist/notion/config.js.map +1 -1
  44. package/dist/notion/content-cache.d.ts +1 -1
  45. package/dist/notion/generic-source.js +0 -1
  46. package/dist/notion/generic-source.js.map +1 -1
  47. package/dist/notion/index.d.ts +4 -4
  48. package/dist/notion/index.js +8 -23
  49. package/dist/notion/index.js.map +1 -1
  50. package/dist/notion/mappers.js.map +1 -1
  51. package/dist/notion/media.d.ts +1 -1
  52. package/dist/notion/media.js +1 -1
  53. package/dist/notion/media.js.map +1 -1
  54. package/dist/notion/property-mappers.d.ts +2 -1
  55. package/dist/notion/property-mappers.js +7 -0
  56. package/dist/notion/property-mappers.js.map +1 -1
  57. package/dist/notion/routes/index.d.ts +1 -1
  58. package/dist/notion/routes/index.js +0 -1
  59. package/dist/notion/routes/index.js.map +1 -1
  60. package/dist/notion/routes/webhook.d.ts +1 -1
  61. package/dist/notion/routes/webhook.js +0 -1
  62. package/dist/notion/routes/webhook.js.map +1 -1
  63. package/dist/notion/types.d.ts +1 -73
  64. package/dist/notion/webhook.d.ts +1 -1
  65. package/dist/notion/webhook.js +0 -1
  66. package/dist/notion/webhook.js.map +1 -1
  67. package/dist/pages/index.d.ts +117 -0
  68. package/dist/pages/index.js +487 -0
  69. package/dist/pages/index.js.map +1 -0
  70. package/dist/platform/current.d.ts +1 -1
  71. package/dist/platform/current.js.map +1 -1
  72. package/dist/platform/index.d.ts +1 -1
  73. package/dist/platform/index.js.map +1 -1
  74. package/dist/platform/runtime.d.ts +1 -1
  75. package/dist/storage/index.js.map +1 -1
  76. package/dist/storage/routes/cdn.js.map +1 -1
  77. package/dist/storage/routes/files.js.map +1 -1
  78. package/dist/storage/routes/index.js.map +1 -1
  79. package/dist/util/index.d.ts +1 -1
  80. package/dist/util/index.js +1 -2
  81. package/dist/util/index.js.map +1 -1
  82. package/dist/worker/index.js +0 -1
  83. package/dist/worker/index.js.map +1 -1
  84. package/dist/worker/routes/content-revalidate.d.ts +1 -1
  85. package/dist/worker/routes/health.js.map +1 -1
  86. package/dist/worker/routes/index.d.ts +1 -1
  87. package/dist/worker/routes/index.js.map +1 -1
  88. package/package.json +14 -1
@@ -16,26 +16,6 @@ function clearRegistryForTests() {
16
16
  registry.length = 0;
17
17
  }
18
18
 
19
- // src/i18n/config.ts
20
- var supportedLocales = ["zh-CN", "en-US"];
21
- function isAppLocale(value) {
22
- return supportedLocales.includes(value);
23
- }
24
- function expandLocalizedMoviePaths(paths, locale) {
25
- const locales = locale && isAppLocale(locale) ? [locale] : [...supportedLocales];
26
- const expanded = [];
27
- for (const path of paths) {
28
- if (path === "/movies" || path.startsWith("/movies/")) {
29
- for (const currentLocale of locales) {
30
- expanded.push(`/${currentLocale}${path}`);
31
- }
32
- continue;
33
- }
34
- expanded.push(path);
35
- }
36
- return Array.from(new Set(expanded));
37
- }
38
-
39
19
  // src/content/revalidate.ts
40
20
  function asObject(input) {
41
21
  return input && typeof input === "object" && !Array.isArray(input) ? input : null;
@@ -73,9 +53,6 @@ function timingSafeEqualString(a, b) {
73
53
  }
74
54
  return diff === 0;
75
55
  }
76
- function shouldLocalizeMoviePaths(modelId) {
77
- return modelId === "movies" || modelId === "movie-translations";
78
- }
79
56
  function buildContentRevalidationPaths(input) {
80
57
  const pagePaths = [input.model.routes.listPath];
81
58
  const routePaths = [];
@@ -92,8 +69,8 @@ function buildContentRevalidationPaths(input) {
92
69
  )
93
70
  );
94
71
  }
95
- if (input.localizedMovieDetailPaths?.length) {
96
- pagePaths.push(...input.localizedMovieDetailPaths);
72
+ if (input.extraPagePaths?.length) {
73
+ pagePaths.push(...input.extraPagePaths);
97
74
  }
98
75
  if (input.includeApi !== false && input.model.routes.publicApiPath) {
99
76
  routePaths.push(input.model.routes.publicApiPath);
@@ -114,11 +91,11 @@ function buildContentRevalidationPaths(input) {
114
91
  );
115
92
  }
116
93
  }
117
- const localizedPagePaths = shouldLocalizeMoviePaths(input.model.id) ? expandLocalizedMoviePaths(pagePaths, input.locale) : pagePaths;
94
+ const expandedPagePaths = input.expandPagePaths ? input.expandPagePaths(pagePaths, input.locale) : pagePaths;
118
95
  return {
119
- pagePaths: Array.from(new Set(localizedPagePaths)),
96
+ pagePaths: Array.from(new Set(expandedPagePaths)),
120
97
  routePaths: Array.from(new Set(routePaths)),
121
- all: Array.from(/* @__PURE__ */ new Set([...localizedPagePaths, ...routePaths]))
98
+ all: Array.from(/* @__PURE__ */ new Set([...expandedPagePaths, ...routePaths]))
122
99
  };
123
100
  }
124
101
  function authorizeContentRevalidate(request, token) {
@@ -192,36 +169,8 @@ function matchesSearchQuery(values, query) {
192
169
  const haystack = searchableText(values);
193
170
  return terms.every((term) => haystack.includes(term));
194
171
  }
195
- function filterPostsBySearch(posts, query) {
196
- return posts.filter(
197
- (post) => matchesSearchQuery(
198
- [
199
- post.title,
200
- post.description,
201
- post.author,
202
- post.tags,
203
- post.slug,
204
- post.date
205
- ],
206
- query
207
- )
208
- );
209
- }
210
- function filterMoviesBySearch(movies, query) {
211
- return movies.filter(
212
- (movie) => matchesSearchQuery(
213
- [
214
- movie.title,
215
- movie.summary,
216
- movie.director,
217
- movie.actors,
218
- movie.genres,
219
- movie.releaseDate,
220
- movie.routeId
221
- ],
222
- query
223
- )
224
- );
172
+ function filterItemsBySearch(items, valuesForItem, query) {
173
+ return items.filter((item) => matchesSearchQuery(valuesForItem(item), query));
225
174
  }
226
175
 
227
176
  // src/content/search-index.ts
@@ -420,6 +369,171 @@ async function prewarmPublicContentSearchIndex(targets, options) {
420
369
  return output;
421
370
  }
422
371
 
372
+ // src/notion/property-mappers.ts
373
+ function isRecord(value) {
374
+ return Boolean(value && typeof value === "object");
375
+ }
376
+ function getPlainText(parts) {
377
+ if (!Array.isArray(parts)) return "";
378
+ return parts.map((part) => part.plain_text ?? "").join("").trim();
379
+ }
380
+ function getProperty(properties, key) {
381
+ return properties[key];
382
+ }
383
+ function getRichTextProperty(properties, key) {
384
+ const property = getProperty(properties, key);
385
+ if (!property) return "";
386
+ if (property.type === "title") return getPlainText(property.title);
387
+ if (property.type === "rich_text") return getPlainText(property.rich_text);
388
+ if (property.type === "url") return String(property.url ?? "").trim();
389
+ if (property.type === "email") return String(property.email ?? "").trim();
390
+ if (property.type === "phone_number") {
391
+ return String(property.phone_number ?? "").trim();
392
+ }
393
+ return "";
394
+ }
395
+ function getSelectProperty(properties, key) {
396
+ const property = getProperty(properties, key);
397
+ if (property?.type !== "select") return "";
398
+ const select = property.select;
399
+ return String(select?.name ?? "").trim();
400
+ }
401
+ function getCheckboxProperty(properties, key) {
402
+ const property = getProperty(properties, key);
403
+ if (property?.type !== "checkbox") return false;
404
+ return Boolean(property.checkbox);
405
+ }
406
+ function getRelationPageIds(properties, key) {
407
+ const property = getProperty(properties, key);
408
+ if (property?.type !== "relation" || !Array.isArray(property.relation)) {
409
+ return [];
410
+ }
411
+ return property.relation.map((item) => String(item.id ?? "").trim()).filter(Boolean);
412
+ }
413
+ function getTagsProperty(properties, key) {
414
+ const property = getProperty(properties, key);
415
+ if (property?.type === "multi_select" && Array.isArray(property.multi_select)) {
416
+ return property.multi_select.map((item) => String(item.name ?? "").trim()).filter(Boolean);
417
+ }
418
+ if (property?.type === "select") {
419
+ const select = property.select;
420
+ const name = String(select?.name ?? "").trim();
421
+ return name ? [name] : [];
422
+ }
423
+ return [];
424
+ }
425
+ function isValidPublicSlug(slug) {
426
+ return /^[a-z0-9][a-z0-9-]{0,79}$/.test(slug);
427
+ }
428
+ function notionPageEditUrl(pageId, editBaseUrl) {
429
+ const compactPageId = pageId.replaceAll("-", "");
430
+ if (editBaseUrl?.includes("{pageId}")) {
431
+ return editBaseUrl.replaceAll("{pageId}", compactPageId);
432
+ }
433
+ return `https://www.notion.so/${compactPageId}`;
434
+ }
435
+ function compactNotionId(id) {
436
+ return id.replaceAll("-", "").toLowerCase();
437
+ }
438
+
439
+ // src/content/localized.ts
440
+ function readPublishedFlag(properties, field) {
441
+ const property = properties[field];
442
+ if (property?.type === "checkbox") return getCheckboxProperty(properties, field);
443
+ if (property?.type === "status") {
444
+ const status = property.status;
445
+ return String(status?.name ?? "").trim().toLowerCase() === "published";
446
+ }
447
+ if (property?.type === "select") {
448
+ const select = property.select;
449
+ return String(select?.name ?? "").trim().toLowerCase() === "published";
450
+ }
451
+ return false;
452
+ }
453
+ function readLocale(properties, field) {
454
+ return getSelectProperty(properties, field) || getRichTextProperty(properties, field);
455
+ }
456
+ function normalizeExtraField(definition) {
457
+ if (typeof definition === "string") return { field: definition, kind: "text" };
458
+ return { field: definition.field, kind: definition.kind ?? "text" };
459
+ }
460
+ function readExtraFields(properties, fields) {
461
+ const result = {};
462
+ for (const [key, definition] of Object.entries(fields ?? {})) {
463
+ const { field, kind } = normalizeExtraField(definition);
464
+ if (kind === "tags") result[key] = getTagsProperty(properties, field);
465
+ else if (kind === "select") result[key] = getSelectProperty(properties, field);
466
+ else if (kind === "checkbox") result[key] = getCheckboxProperty(properties, field);
467
+ else result[key] = getRichTextProperty(properties, field);
468
+ }
469
+ return result;
470
+ }
471
+ function mapNotionPageToLocalizedContentTranslation(page, input) {
472
+ const properties = isRecord(page.properties) ? page.properties : {};
473
+ const sourcePageIds = getRelationPageIds(properties, input.fields.source);
474
+ const locale = readLocale(properties, input.fields.locale);
475
+ const configuredSlug = getRichTextProperty(
476
+ properties,
477
+ input.fields.slug
478
+ ).toLowerCase();
479
+ const slug = isValidPublicSlug(configuredSlug) ? configuredSlug : "";
480
+ const title = getRichTextProperty(properties, input.fields.title);
481
+ const published = readPublishedFlag(properties, input.fields.published);
482
+ const sourceUrl = typeof page.public_url === "string" && page.public_url ? page.public_url : typeof page.url === "string" ? page.url : null;
483
+ if (!sourcePageIds[0] || !locale || !slug || !title || !published || input.isValidLocale && !input.isValidLocale(locale)) {
484
+ return null;
485
+ }
486
+ return {
487
+ pageId: page.id,
488
+ ...page.last_edited_time ? { updatedAt: page.last_edited_time } : {},
489
+ sourcePageId: sourcePageIds[0],
490
+ locale,
491
+ slug,
492
+ title,
493
+ seoTitle: input.fields.seoTitle ? getRichTextProperty(properties, input.fields.seoTitle) : "",
494
+ seoDescription: input.fields.seoDescription ? getRichTextProperty(properties, input.fields.seoDescription) : "",
495
+ published,
496
+ editUrl: notionPageEditUrl(page.id, input.editBaseUrl),
497
+ sourceUrl,
498
+ ...readExtraFields(properties, input.extraFields)
499
+ };
500
+ }
501
+ function localizeContentList(input) {
502
+ const translationsForLocale = input.translations.filter(
503
+ (translation) => input.getTranslationLocale(translation) === input.locale
504
+ );
505
+ if (translationsForLocale.length === 0 && input.locale === input.defaultLocale) {
506
+ const fallback = input.baseItems.map(input.fallback);
507
+ return input.sort ? fallback.sort(input.sort) : fallback;
508
+ }
509
+ const baseByPageId = new Map(
510
+ input.baseItems.map((item) => [compactNotionId(input.getBasePageId(item)), item])
511
+ );
512
+ const localized = translationsForLocale.map((translation) => {
513
+ const base = baseByPageId.get(
514
+ compactNotionId(input.getTranslationSourcePageId(translation))
515
+ );
516
+ return base ? input.applyTranslation(base, translation) : null;
517
+ }).filter((item) => Boolean(item));
518
+ return input.sort ? localized.sort(input.sort) : localized;
519
+ }
520
+ function getAlternateLocalizedContentLinks(input) {
521
+ const normalizedSourcePageId = compactNotionId(input.sourcePageId);
522
+ return input.translations.filter((translation) => {
523
+ const locale = input.getTranslationLocale(translation);
524
+ return compactNotionId(input.getTranslationSourcePageId(translation)) === normalizedSourcePageId && locale !== input.currentLocale && (!input.isValidLocale || input.isValidLocale(locale));
525
+ }).map((translation) => {
526
+ const locale = input.getTranslationLocale(translation);
527
+ const slug = input.getTranslationSlug(translation);
528
+ return {
529
+ locale,
530
+ slug,
531
+ href: input.hrefForTranslation(locale, slug),
532
+ label: input.labelForLocale?.(locale) ?? locale
533
+ };
534
+ });
535
+ }
536
+
423
537
  // src/content/admin-summary.ts
424
538
  function visibilityFor(model) {
425
539
  if (model.visibility.public && model.visibility.admin) return "public+admin";
@@ -452,13 +566,15 @@ export {
452
566
  defineContentSource,
453
567
  deleteSearchIndexDocument,
454
568
  deleteSearchIndexForModel,
569
+ filterItemsBySearch,
455
570
  filterItemsBySearchIndex,
456
- filterMoviesBySearch,
457
- filterPostsBySearch,
571
+ getAlternateLocalizedContentLinks,
458
572
  getContentModelAdminSummaries,
459
573
  getMissingSearchIndexRouteIds,
460
574
  getRegisteredSource,
461
575
  getRegisteredSources,
576
+ localizeContentList,
577
+ mapNotionPageToLocalizedContentTranslation,
462
578
  matchesIndexedItem,
463
579
  matchesSearchQuery,
464
580
  normalizeSearchQuery,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/content/models.ts","../../src/i18n/config.ts","../../src/content/revalidate.ts","../../src/content/search.ts","../../src/content/search-index.ts","../../src/content/prewarm.ts","../../src/content/admin-summary.ts"],"sourcesContent":["// packages/nextion/src/content/models.ts\n//\n// Canonical content-source shape and a module-level registry.\n//\n// The starter's `defineContentModel` returns the same value passed in.\n// The foundation's `defineContentSource` adds a side effect: it stores\n// the value in a process-wide registry so other packages (admin pages,\n// search index, revalidation) can discover content sources without\n// reaching back into the starter.\n//\n// Existing values from the starter pass through unchanged: `ContentSource`\n// is a structural alias of the prior `ContentModelDefinition<TFields>` so\n// source field-maps stay narrowly typed through the boundary.\n\nimport type {\n NotionFieldMap,\n NotionSort,\n NotionSortDirection,\n} from \"../notion/types\";\n\nexport type { NotionFieldMap, NotionSort, NotionSortDirection };\n\n/**\n * Canonical content-source shape. The shape mirrors the starter's\n * prior `ContentModelDefinition` exactly so that registered sources\n * remain type-compatible across the package boundary.\n */\nexport type ContentModelDefinition<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = {\n id: string;\n kind: \"article\" | \"catalog\" | \"directory\";\n visibility: {\n public: boolean;\n admin: boolean;\n };\n source: {\n type: \"notion\";\n tokenEnv: \"NOTION_TOKEN\";\n dataSourceEnv: string;\n defaultDataSourceId?: string;\n fields: TFields;\n query: {\n pageSize: number;\n sorts?: readonly NotionSort[];\n filterProperties?: readonly string[];\n };\n };\n routes: {\n listPath: string;\n detailPath: string;\n detailParam: string;\n publicApiPath?: string;\n };\n ui: {\n name: string;\n pluralName: string;\n navLabel: string;\n listTitle: string;\n listDescription: string;\n emptyState: string;\n };\n capabilities: {\n richBlocks: boolean;\n coverImages: boolean;\n gatedAssets: boolean;\n };\n};\n\n/**\n * Public alias for `ContentModelDefinition`. External consumers import\n * this name from `@notionx/core/content`; the internal\n * `ContentModelDefinition` name remains available for the starter's\n * `model.ts`.\n */\nexport type ContentSource<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = ContentModelDefinition<TFields>;\n\nconst registry: ContentSource[] = [];\n\n/**\n * Register a content source. Returns the value unchanged. Re-registering\n * the same `id` replaces the prior value (idempotent on the id, useful\n * for HMR + tests).\n */\nexport function defineContentSource<const TFields extends NotionFieldMap>(\n model: ContentModelDefinition<TFields>\n): ContentModelDefinition<TFields> {\n const existing = registry.findIndex((s) => s.id === model.id);\n if (existing >= 0) registry[existing] = model;\n else registry.push(model);\n return model;\n}\n\nexport function getRegisteredSources(): readonly ContentSource[] {\n return registry;\n}\n\nexport function getRegisteredSource(id: string): ContentSource | undefined {\n return registry.find((s) => s.id === id);\n}\n\n/**\n * Test-only escape hatch: empties the registry so vitest cases do not\n * leak state between files. Not for production use.\n */\nexport function clearRegistryForTests(): void {\n registry.length = 0;\n}\n","export const supportedLocales = [\"zh-CN\", \"en-US\"] as const;\n\nexport type AppLocale = (typeof supportedLocales)[number];\n\nexport const defaultLocale: AppLocale = \"zh-CN\";\n\nexport function isAppLocale(value: string): value is AppLocale {\n return (supportedLocales as readonly string[]).includes(value);\n}\n\nexport function localizedMovieListPath(locale: AppLocale) {\n return `/${locale}/movies`;\n}\n\nexport function localizedMovieDetailPath(locale: AppLocale, slug: string) {\n return `/${locale}/movies/${slug}`;\n}\n\nexport function expandLocalizedMoviePaths(\n paths: readonly string[],\n locale?: string\n) {\n const locales =\n locale && isAppLocale(locale) ? [locale] : [...supportedLocales];\n const expanded: string[] = [];\n\n for (const path of paths) {\n if (path === \"/movies\" || path.startsWith(\"/movies/\")) {\n for (const currentLocale of locales) {\n expanded.push(`/${currentLocale}${path}`);\n }\n continue;\n }\n expanded.push(path);\n }\n\n return Array.from(new Set(expanded));\n}\n","// packages/nextion/src/content/revalidate.ts\n//\n// Generic content revalidation helpers. The starter's\n// `revalidateContentModel` is project-specific (it knows how to\n// resolve localized movie paths and which content models exist) and\n// lives in the starter. The helpers below are model-agnostic and are\n// re-exported by the starter's `lib/content/revalidate.ts`.\n\nimport { expandLocalizedMoviePaths } from \"../i18n/config\";\nimport type { ContentModelDefinition } from \"./models\";\nimport { getRegisteredSource } from \"./models\";\n\ntype RevalidatePathFn = (\n path: string,\n type?: \"page\" | \"layout\"\n) => void | Promise<void>;\n\nexport type { RevalidatePathFn };\n\nexport type InvalidationKind = \"publish\" | \"update\" | \"delete\";\n\nexport type ContentRevalidateRequest = {\n modelId: string;\n pageId?: string;\n routeId?: string;\n previousRouteId?: string;\n locale?: string;\n kind?: InvalidationKind;\n includeApi?: boolean;\n};\n\nfunction asObject(input: unknown): Record<string, unknown> | null {\n return input && typeof input === \"object\" && !Array.isArray(input)\n ? (input as Record<string, unknown>)\n : null;\n}\n\nfunction readString(input: Record<string, unknown>, key: string) {\n const value = input[key];\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\nfunction readKind(input: Record<string, unknown>): InvalidationKind {\n const value = readString(input, \"kind\");\n if (value === \"publish\" || value === \"delete\" || value === \"update\") {\n return value;\n }\n return \"update\";\n}\n\nfunction detailPathForRouteId(detailPath: string, routeId: string) {\n return detailPath.replace(/\\[[^\\]]+\\]/g, routeId);\n}\n\nfunction publicApiDetailPathForRouteId(publicApiPath: string, routeId: string) {\n return `${publicApiPath.replace(/\\/+$/, \"\")}/${routeId.replace(/^\\/+/, \"\")}`;\n}\n\nfunction bearerToken(request: Request) {\n const authorization = request.headers.get(\"authorization\") ?? \"\";\n const match = authorization.match(/^Bearer\\s+(.+)$/i);\n return match?.[1]?.trim() ?? \"\";\n}\n\nfunction timingSafeEqualString(a: string, b: string) {\n const encoder = new TextEncoder();\n const left = encoder.encode(a);\n const right = encoder.encode(b);\n if (left.byteLength !== right.byteLength) return false;\n\n let diff = 0;\n for (let index = 0; index < left.byteLength; index += 1) {\n diff |= (left[index] ?? 0) ^ (right[index] ?? 0);\n }\n return diff === 0;\n}\n\nfunction shouldLocalizeMoviePaths(modelId: string) {\n return modelId === \"movies\" || modelId === \"movie-translations\";\n}\n\nexport function buildContentRevalidationPaths(input: {\n model: Pick<ContentModelDefinition, \"id\" | \"routes\">;\n routeId?: string;\n previousRouteId?: string;\n locale?: string;\n includeApi?: boolean;\n localizedMovieDetailPaths?: readonly string[];\n}) {\n const pagePaths = [input.model.routes.listPath];\n const routePaths: string[] = [];\n\n if (input.routeId) {\n pagePaths.push(\n detailPathForRouteId(input.model.routes.detailPath, input.routeId)\n );\n }\n if (input.previousRouteId) {\n pagePaths.push(\n detailPathForRouteId(\n input.model.routes.detailPath,\n input.previousRouteId\n )\n );\n }\n if (input.localizedMovieDetailPaths?.length) {\n pagePaths.push(...input.localizedMovieDetailPaths);\n }\n if (input.includeApi !== false && input.model.routes.publicApiPath) {\n routePaths.push(input.model.routes.publicApiPath);\n if (input.routeId) {\n routePaths.push(\n publicApiDetailPathForRouteId(\n input.model.routes.publicApiPath,\n input.routeId\n )\n );\n }\n if (input.previousRouteId) {\n routePaths.push(\n publicApiDetailPathForRouteId(\n input.model.routes.publicApiPath,\n input.previousRouteId\n )\n );\n }\n }\n\n const localizedPagePaths = shouldLocalizeMoviePaths(input.model.id)\n ? expandLocalizedMoviePaths(pagePaths, input.locale)\n : pagePaths;\n\n return {\n pagePaths: Array.from(new Set(localizedPagePaths)),\n routePaths: Array.from(new Set(routePaths)),\n all: Array.from(new Set([...localizedPagePaths, ...routePaths])),\n };\n}\n\nexport function authorizeContentRevalidate(\n request: Request,\n token?: string | null\n) {\n const expected = String(token ?? \"\").trim();\n if (!expected) return false;\n const actual = bearerToken(request);\n return Boolean(actual && timingSafeEqualString(actual, expected));\n}\n\nexport async function readContentRevalidateRequest(\n request: Request\n): Promise<ContentRevalidateRequest | null> {\n const body = asObject(await request.json().catch(() => null));\n if (!body) return null;\n\n const modelId = readString(body, \"modelId\");\n const pageId = readString(body, \"pageId\");\n const routeId = readString(body, \"routeId\");\n const previousRouteId = readString(body, \"previousRouteId\");\n const locale = readString(body, \"locale\");\n\n return {\n modelId,\n pageId: pageId || undefined,\n routeId: routeId || undefined,\n previousRouteId: previousRouteId || undefined,\n locale: locale || undefined,\n kind: readKind(body),\n includeApi: body.includeApi !== false,\n };\n}\n\nexport function readContentRevalidateRequestFromUrl(\n url: URL\n): ContentRevalidateRequest | null {\n const modelId = url.searchParams.get(\"modelId\")?.trim() ?? \"\";\n if (!modelId) return null;\n\n const kind = url.searchParams.get(\"kind\")?.trim() ?? \"\";\n const includeApi = url.searchParams.get(\"includeApi\");\n return {\n modelId,\n pageId: url.searchParams.get(\"pageId\")?.trim() || undefined,\n routeId: url.searchParams.get(\"routeId\")?.trim() || undefined,\n previousRouteId:\n url.searchParams.get(\"previousRouteId\")?.trim() || undefined,\n locale: url.searchParams.get(\"locale\")?.trim() || undefined,\n kind:\n kind === \"publish\" || kind === \"delete\" || kind === \"update\"\n ? kind\n : \"update\",\n includeApi: includeApi !== \"false\",\n };\n}\n\nexport function previewContentModelInvalidation(input: ContentRevalidateRequest) {\n const model = getRegisteredSource(input.modelId);\n if (!model) {\n throw new Error(`Unknown content model: ${input.modelId}`);\n }\n\n return buildContentRevalidationPaths({\n model,\n routeId: input.routeId,\n previousRouteId: input.previousRouteId,\n includeApi: input.includeApi,\n });\n}\n","// packages/nextion/src/content/search.ts\n//\n// Generic text-search helpers for content sources.\n\nimport type {\n NotionMovieListItem,\n NotionPostListItem,\n} from \"../notion/types\";\n\nexport function normalizeSearchQuery(query: string | null | undefined) {\n return String(query ?? \"\")\n .normalize(\"NFKC\")\n .trim()\n .replace(/\\s+/g, \" \")\n .toLowerCase();\n}\n\nfunction searchTerms(query: string | null | undefined) {\n const normalized = normalizeSearchQuery(query);\n return normalized ? normalized.split(\" \") : [];\n}\n\nfunction searchableText(values: readonly unknown[]) {\n return values\n .flatMap((value) => (Array.isArray(value) ? value : [value]))\n .filter((value): value is string | number | boolean =>\n [\"string\", \"number\", \"boolean\"].includes(typeof value)\n )\n .map((value) => String(value))\n .join(\" \")\n .normalize(\"NFKC\")\n .toLowerCase();\n}\n\nexport function matchesSearchQuery(\n values: readonly unknown[],\n query: string | null | undefined\n) {\n const terms = searchTerms(query);\n if (terms.length === 0) return true;\n\n const haystack = searchableText(values);\n return terms.every((term) => haystack.includes(term));\n}\n\nexport function filterPostsBySearch<TPost extends NotionPostListItem>(\n posts: readonly TPost[],\n query: string | null | undefined\n) {\n return posts.filter((post) =>\n matchesSearchQuery(\n [\n post.title,\n post.description,\n post.author,\n post.tags,\n post.slug,\n post.date,\n ],\n query\n )\n );\n}\n\nexport function filterMoviesBySearch<TMovie extends NotionMovieListItem>(\n movies: readonly TMovie[],\n query: string | null | undefined\n) {\n return movies.filter((movie) =>\n matchesSearchQuery(\n [\n movie.title,\n movie.summary,\n movie.director,\n movie.actors,\n movie.genres,\n movie.releaseDate,\n movie.routeId,\n ],\n query\n )\n );\n}\n","// packages/nextion/src/content/search-index.ts\n//\n// Generic D1-backed content search index helpers.\n\nimport type { SqlDatabaseAdapter } from \"../platform/runtime\";\nimport { matchesSearchQuery, normalizeSearchQuery } from \"./search\";\n\nexport type SearchIndexedItem = {\n pageId?: string;\n slug?: string;\n routeId?: string;\n title: string;\n description?: string;\n date?: string;\n author?: string;\n tags?: readonly string[];\n releaseDate?: string;\n director?: string;\n actors?: string;\n summary?: string;\n genres?: readonly string[];\n};\n\nexport type SearchIndexDocument = {\n modelId: string;\n pageId: string;\n routeId: string;\n title: string;\n summary: string;\n bodyText: string;\n facets: readonly string[];\n sourceUpdatedAt?: string | null;\n};\n\ntype SearchIndexRow = {\n route_id: string;\n};\n\ntype MaybePromise<T> = T | Promise<T>;\n\nfunction indexValuesForItem(item: SearchIndexedItem) {\n return [\n item.title,\n item.description,\n item.author,\n item.tags,\n item.slug,\n item.date,\n item.summary,\n item.director,\n item.actors,\n item.genres,\n item.routeId,\n item.releaseDate,\n ];\n}\n\nfunction routeIdForItem(item: SearchIndexedItem) {\n return item.routeId ?? item.slug ?? \"\";\n}\n\nfunction routeOrder<T extends SearchIndexedItem>(items: readonly T[]) {\n const order = new Map<string, number>();\n items.forEach((item, index) => {\n const routeId = routeIdForItem(item);\n if (routeId) order.set(routeId, index);\n });\n return order;\n}\n\nfunction uniqueValues(values: readonly string[]) {\n return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));\n}\n\nexport async function upsertSearchIndexDocument(\n db: SqlDatabaseAdapter,\n document: SearchIndexDocument\n) {\n const normalizedText = [\n document.title,\n document.summary,\n document.bodyText,\n ...document.facets,\n ]\n .join(\" \")\n .normalize(\"NFKC\")\n .replace(/\\s+/g, \" \")\n .toLowerCase();\n\n await db\n .prepare(\n `INSERT INTO content_search_index (\n model_id,\n page_id,\n route_id,\n title,\n summary,\n body_text,\n facets,\n normalized_text,\n source_updated_at,\n indexed_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))\n ON CONFLICT(model_id, route_id) DO UPDATE SET\n page_id = excluded.page_id,\n title = excluded.title,\n summary = excluded.summary,\n body_text = excluded.body_text,\n facets = excluded.facets,\n normalized_text = excluded.normalized_text,\n source_updated_at = excluded.source_updated_at,\n indexed_at = excluded.indexed_at`\n )\n .bind(\n document.modelId,\n document.pageId,\n document.routeId,\n document.title,\n document.summary,\n document.bodyText,\n JSON.stringify(uniqueValues([...document.facets])),\n normalizedText,\n document.sourceUpdatedAt ?? null\n )\n .run();\n}\n\nexport async function deleteSearchIndexDocument(\n db: SqlDatabaseAdapter,\n input: { modelId: string; routeId: string }\n) {\n await db\n .prepare(\n \"DELETE FROM content_search_index WHERE model_id = ? AND route_id = ?\"\n )\n .bind(input.modelId, input.routeId)\n .run();\n}\n\nexport async function deleteSearchIndexForModel(\n db: SqlDatabaseAdapter,\n input: { modelId: string }\n) {\n await db\n .prepare(\"DELETE FROM content_search_index WHERE model_id = ?\")\n .bind(input.modelId)\n .run();\n}\n\nexport async function getMissingSearchIndexRouteIds(\n db: SqlDatabaseAdapter,\n input: { modelId: string; routeIds: readonly string[] }\n) {\n const routeIds = uniqueValues([...input.routeIds]);\n if (routeIds.length === 0) return [];\n\n const placeholders = routeIds.map(() => \"?\").join(\", \");\n const result = await db\n .prepare(\n `SELECT route_id\n FROM content_search_index\n WHERE model_id = ? AND route_id IN (${placeholders})`\n )\n .bind(input.modelId, ...routeIds)\n .all<SearchIndexRow>();\n\n const present = new Set((result.results ?? []).map((row) => row.route_id));\n return routeIds.filter((routeId) => !present.has(routeId));\n}\n\nexport async function querySearchIndexRouteIds(\n db: SqlDatabaseAdapter,\n input: { modelId: string; query: string; limit?: number }\n) {\n const query = normalizeSearchQuery(input.query);\n if (!query) return [];\n\n const terms = query\n .split(\" \")\n .map((term) => `%${term}%`);\n const clauses = terms\n .map(\n () =>\n \"(normalized_text LIKE ? OR title LIKE ? OR summary LIKE ? OR facets LIKE ?)\"\n )\n .join(\" AND \");\n const values = terms.flatMap((term) => [term, term, term, term]);\n\n const result = await db\n .prepare(\n `SELECT route_id\n FROM content_search_index\n WHERE model_id = ? AND ${clauses}\n ORDER BY indexed_at DESC\n LIMIT ?`\n )\n .bind(input.modelId, ...values, input.limit ?? 200)\n .all<SearchIndexRow>();\n\n return (result.results ?? [])\n .map((row) => row.route_id)\n .filter((routeId): routeId is string => typeof routeId === \"string\");\n}\n\nexport async function filterItemsBySearchIndex<T extends SearchIndexedItem>(\n items: readonly T[],\n query: string | null | undefined,\n input: {\n modelId: string;\n filterFallback: (items: readonly T[], query: string | null | undefined) => T[];\n getDatabase?: () => MaybePromise<SqlDatabaseAdapter | null>;\n }\n) {\n const normalized = normalizeSearchQuery(query);\n if (!normalized) return [...items];\n if (!input.getDatabase) return input.filterFallback(items, normalized);\n\n try {\n const db = await input.getDatabase();\n if (!db) return input.filterFallback(items, normalized);\n const routeIds = await querySearchIndexRouteIds(db, {\n modelId: input.modelId,\n query: normalized,\n limit: Math.max(items.length, 200),\n });\n if (routeIds.length === 0) return input.filterFallback(items, normalized);\n\n const order = routeOrder(items);\n const matched = new Set(routeIds);\n return items\n .filter((item) => matched.has(routeIdForItem(item)))\n .sort(\n (a, b) =>\n (order.get(routeIdForItem(a)) ?? 0) -\n (order.get(routeIdForItem(b)) ?? 0)\n );\n } catch (error) {\n console.warn(\n JSON.stringify({\n tag: \"content_search_index_error\",\n modelId: input.modelId,\n message: error instanceof Error ? error.message : String(error),\n })\n );\n return input.filterFallback(items, normalized);\n }\n}\n\nexport function matchesIndexedItem(\n item: SearchIndexedItem,\n query: string | null | undefined\n) {\n return matchesSearchQuery(indexValuesForItem(item), query);\n}\n","// packages/nextion/src/content/prewarm.ts\n//\n// Generic content search index prewarming. The function takes a list\n// of (modelId, runner) targets and runs them. The starter wires the\n// project-specific runners in its `lib/content/prewarm.ts` shim.\n\nexport type ContentPrewarmModelResult = {\n modelId: string;\n ok: boolean;\n total: number;\n indexed: number;\n skipped: boolean;\n error?: string;\n};\n\nexport type ContentPrewarmResult = {\n ok: boolean;\n startedAt: string;\n finishedAt: string;\n durationMs: number;\n models: ContentPrewarmModelResult[];\n};\n\nexport type PrewarmTarget = {\n modelId: string;\n run: () => Promise<{ total: number; indexed: number; skipped: boolean }>;\n};\n\nfunction logContentPrewarm(fields: Record<string, unknown>) {\n try {\n console.log(JSON.stringify({ tag: \"content_prewarm\", ...fields }));\n } catch {\n // Ignore logging serialization errors.\n }\n}\n\nexport async function prewarmPublicContentSearchIndex(\n targets: readonly PrewarmTarget[],\n options?: { models?: readonly string[] }\n): Promise<ContentPrewarmResult> {\n const startedAtDate = new Date();\n const startedAt = startedAtDate.toISOString();\n const t0 = performance.now();\n const requestedModels = new Set(options?.models?.filter(Boolean));\n const selected =\n requestedModels.size > 0\n ? targets.filter((target) => requestedModels.has(target.modelId))\n : targets;\n\n const models: ContentPrewarmModelResult[] = [];\n for (const target of selected) {\n try {\n const result = await target.run();\n models.push({\n modelId: target.modelId,\n ok: true,\n total: result.total,\n indexed: result.indexed,\n skipped: result.skipped,\n });\n } catch (error) {\n models.push({\n modelId: target.modelId,\n ok: false,\n total: 0,\n indexed: 0,\n skipped: true,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n const finishedAt = new Date().toISOString();\n const output: ContentPrewarmResult = {\n ok: models.every((model) => model.ok),\n startedAt,\n finishedAt,\n durationMs: Math.round((performance.now() - t0) * 100) / 100,\n models,\n };\n\n logContentPrewarm({\n ok: output.ok,\n started_at: output.startedAt,\n finished_at: output.finishedAt,\n duration_ms: output.durationMs,\n models: output.models,\n });\n\n return output;\n}\n","// packages/nextion/src/content/admin-summary.ts\n//\n// Generic admin summary helpers for content sources.\n\nimport type {\n ContentModelDefinition,\n NotionFieldMap,\n} from \"./models\";\nimport { getRegisteredSources } from \"./models\";\n\nexport type ContentModelAdminSummary = {\n id: string;\n name: string;\n kind: ContentModelDefinition[\"kind\"];\n visibility: \"public\" | \"admin\" | \"public+admin\" | \"private\";\n listPath: string;\n detailPath: string;\n publicApiPath?: string;\n dataSourceEnv: string;\n hasDefaultDataSource: boolean;\n fieldCount: number;\n capabilities: {\n richBlocks: boolean;\n coverImages: boolean;\n gatedAssets: boolean;\n };\n};\n\nfunction visibilityFor(model: ContentModelDefinition<NotionFieldMap>) {\n if (model.visibility.public && model.visibility.admin) return \"public+admin\";\n if (model.visibility.public) return \"public\";\n if (model.visibility.admin) return \"admin\";\n return \"private\";\n}\n\nexport function summarizeContentModelForAdmin(\n model: ContentModelDefinition<NotionFieldMap>\n): ContentModelAdminSummary {\n return {\n id: model.id,\n name: model.ui.name,\n kind: model.kind,\n visibility: visibilityFor(model),\n listPath: model.routes.listPath,\n detailPath: model.routes.detailPath,\n publicApiPath: model.routes.publicApiPath,\n dataSourceEnv: model.source.dataSourceEnv,\n hasDefaultDataSource: Boolean(model.source.defaultDataSourceId),\n fieldCount: Object.keys(model.source.fields).length,\n capabilities: model.capabilities,\n };\n}\n\nexport function getContentModelAdminSummaries(\n models: readonly ContentModelDefinition<NotionFieldMap>[] = getRegisteredSources()\n) {\n return models.map(summarizeContentModelForAdmin);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/content/models.ts","../../src/content/revalidate.ts","../../src/content/search.ts","../../src/content/search-index.ts","../../src/content/prewarm.ts","../../src/notion/property-mappers.ts","../../src/content/localized.ts","../../src/content/admin-summary.ts"],"sourcesContent":["// packages/nextion/src/content/models.ts\n//\n// Canonical content-source shape and a module-level registry.\n//\n// The starter's `defineContentModel` returns the same value passed in.\n// The foundation's `defineContentSource` adds a side effect: it stores\n// the value in a process-wide registry so other packages (admin pages,\n// search index, revalidation) can discover content sources without\n// reaching back into the starter.\n//\n// Existing values from the starter pass through unchanged: `ContentSource`\n// is a structural alias of the prior `ContentModelDefinition<TFields>` so\n// source field-maps stay narrowly typed through the boundary.\n\nimport type {\n NotionFieldMap,\n NotionSort,\n NotionSortDirection,\n} from \"../notion/types\";\n\nexport type { NotionFieldMap, NotionSort, NotionSortDirection };\n\n/**\n * Canonical content-source shape. The shape mirrors the starter's\n * prior `ContentModelDefinition` exactly so that registered sources\n * remain type-compatible across the package boundary.\n */\nexport type ContentModelDefinition<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = {\n id: string;\n kind: \"article\" | \"catalog\" | \"directory\";\n visibility: {\n public: boolean;\n admin: boolean;\n };\n source: {\n type: \"notion\";\n tokenEnv: \"NOTION_TOKEN\";\n dataSourceEnv: string;\n defaultDataSourceId?: string;\n fields: TFields;\n query: {\n pageSize: number;\n sorts?: readonly NotionSort[];\n filterProperties?: readonly string[];\n };\n };\n routes: {\n listPath: string;\n detailPath: string;\n detailParam: string;\n publicApiPath?: string;\n };\n ui: {\n name: string;\n pluralName: string;\n navLabel: string;\n listTitle: string;\n listDescription: string;\n emptyState: string;\n };\n capabilities: {\n richBlocks: boolean;\n coverImages: boolean;\n gatedAssets: boolean;\n };\n};\n\n/**\n * Public alias for `ContentModelDefinition`. External consumers import\n * this name from `@notionx/core/content`; the internal\n * `ContentModelDefinition` name remains available for the starter's\n * `model.ts`.\n */\nexport type ContentSource<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = ContentModelDefinition<TFields>;\n\nconst registry: ContentSource[] = [];\n\n/**\n * Register a content source. Returns the value unchanged. Re-registering\n * the same `id` replaces the prior value (idempotent on the id, useful\n * for HMR + tests).\n */\nexport function defineContentSource<const TFields extends NotionFieldMap>(\n model: ContentModelDefinition<TFields>\n): ContentModelDefinition<TFields> {\n const existing = registry.findIndex((s) => s.id === model.id);\n if (existing >= 0) registry[existing] = model;\n else registry.push(model);\n return model;\n}\n\nexport function getRegisteredSources(): readonly ContentSource[] {\n return registry;\n}\n\nexport function getRegisteredSource(id: string): ContentSource | undefined {\n return registry.find((s) => s.id === id);\n}\n\n/**\n * Test-only escape hatch: empties the registry so vitest cases do not\n * leak state between files. Not for production use.\n */\nexport function clearRegistryForTests(): void {\n registry.length = 0;\n}\n","// packages/nextion/src/content/revalidate.ts\n//\n// Generic content revalidation helpers. Projects can layer domain-specific\n// path expansion (for example localized routes) through `expandPagePaths`.\n\nimport type { ContentModelDefinition } from \"./models\";\nimport { getRegisteredSource } from \"./models\";\n\ntype RevalidatePathFn = (\n path: string,\n type?: \"page\" | \"layout\"\n) => void | Promise<void>;\n\nexport type { RevalidatePathFn };\n\nexport type InvalidationKind = \"publish\" | \"update\" | \"delete\";\n\nexport type ContentRevalidateRequest = {\n modelId: string;\n pageId?: string;\n routeId?: string;\n previousRouteId?: string;\n locale?: string;\n kind?: InvalidationKind;\n includeApi?: boolean;\n};\n\nfunction asObject(input: unknown): Record<string, unknown> | null {\n return input && typeof input === \"object\" && !Array.isArray(input)\n ? (input as Record<string, unknown>)\n : null;\n}\n\nfunction readString(input: Record<string, unknown>, key: string) {\n const value = input[key];\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\nfunction readKind(input: Record<string, unknown>): InvalidationKind {\n const value = readString(input, \"kind\");\n if (value === \"publish\" || value === \"delete\" || value === \"update\") {\n return value;\n }\n return \"update\";\n}\n\nfunction detailPathForRouteId(detailPath: string, routeId: string) {\n return detailPath.replace(/\\[[^\\]]+\\]/g, routeId);\n}\n\nfunction publicApiDetailPathForRouteId(publicApiPath: string, routeId: string) {\n return `${publicApiPath.replace(/\\/+$/, \"\")}/${routeId.replace(/^\\/+/, \"\")}`;\n}\n\nfunction bearerToken(request: Request) {\n const authorization = request.headers.get(\"authorization\") ?? \"\";\n const match = authorization.match(/^Bearer\\s+(.+)$/i);\n return match?.[1]?.trim() ?? \"\";\n}\n\nfunction timingSafeEqualString(a: string, b: string) {\n const encoder = new TextEncoder();\n const left = encoder.encode(a);\n const right = encoder.encode(b);\n if (left.byteLength !== right.byteLength) return false;\n\n let diff = 0;\n for (let index = 0; index < left.byteLength; index += 1) {\n diff |= (left[index] ?? 0) ^ (right[index] ?? 0);\n }\n return diff === 0;\n}\n\nexport function buildContentRevalidationPaths(input: {\n model: Pick<ContentModelDefinition, \"id\" | \"routes\">;\n routeId?: string;\n previousRouteId?: string;\n locale?: string;\n includeApi?: boolean;\n extraPagePaths?: readonly string[];\n expandPagePaths?: (paths: readonly string[], locale?: string) => readonly string[];\n}) {\n const pagePaths = [input.model.routes.listPath];\n const routePaths: string[] = [];\n\n if (input.routeId) {\n pagePaths.push(\n detailPathForRouteId(input.model.routes.detailPath, input.routeId)\n );\n }\n if (input.previousRouteId) {\n pagePaths.push(\n detailPathForRouteId(\n input.model.routes.detailPath,\n input.previousRouteId\n )\n );\n }\n if (input.extraPagePaths?.length) {\n pagePaths.push(...input.extraPagePaths);\n }\n if (input.includeApi !== false && input.model.routes.publicApiPath) {\n routePaths.push(input.model.routes.publicApiPath);\n if (input.routeId) {\n routePaths.push(\n publicApiDetailPathForRouteId(\n input.model.routes.publicApiPath,\n input.routeId\n )\n );\n }\n if (input.previousRouteId) {\n routePaths.push(\n publicApiDetailPathForRouteId(\n input.model.routes.publicApiPath,\n input.previousRouteId\n )\n );\n }\n }\n\n const expandedPagePaths = input.expandPagePaths\n ? input.expandPagePaths(pagePaths, input.locale)\n : pagePaths;\n\n return {\n pagePaths: Array.from(new Set(expandedPagePaths)),\n routePaths: Array.from(new Set(routePaths)),\n all: Array.from(new Set([...expandedPagePaths, ...routePaths])),\n };\n}\n\nexport function authorizeContentRevalidate(\n request: Request,\n token?: string | null\n) {\n const expected = String(token ?? \"\").trim();\n if (!expected) return false;\n const actual = bearerToken(request);\n return Boolean(actual && timingSafeEqualString(actual, expected));\n}\n\nexport async function readContentRevalidateRequest(\n request: Request\n): Promise<ContentRevalidateRequest | null> {\n const body = asObject(await request.json().catch(() => null));\n if (!body) return null;\n\n const modelId = readString(body, \"modelId\");\n const pageId = readString(body, \"pageId\");\n const routeId = readString(body, \"routeId\");\n const previousRouteId = readString(body, \"previousRouteId\");\n const locale = readString(body, \"locale\");\n\n return {\n modelId,\n pageId: pageId || undefined,\n routeId: routeId || undefined,\n previousRouteId: previousRouteId || undefined,\n locale: locale || undefined,\n kind: readKind(body),\n includeApi: body.includeApi !== false,\n };\n}\n\nexport function readContentRevalidateRequestFromUrl(\n url: URL\n): ContentRevalidateRequest | null {\n const modelId = url.searchParams.get(\"modelId\")?.trim() ?? \"\";\n if (!modelId) return null;\n\n const kind = url.searchParams.get(\"kind\")?.trim() ?? \"\";\n const includeApi = url.searchParams.get(\"includeApi\");\n return {\n modelId,\n pageId: url.searchParams.get(\"pageId\")?.trim() || undefined,\n routeId: url.searchParams.get(\"routeId\")?.trim() || undefined,\n previousRouteId:\n url.searchParams.get(\"previousRouteId\")?.trim() || undefined,\n locale: url.searchParams.get(\"locale\")?.trim() || undefined,\n kind:\n kind === \"publish\" || kind === \"delete\" || kind === \"update\"\n ? kind\n : \"update\",\n includeApi: includeApi !== \"false\",\n };\n}\n\nexport function previewContentModelInvalidation(input: ContentRevalidateRequest) {\n const model = getRegisteredSource(input.modelId);\n if (!model) {\n throw new Error(`Unknown content model: ${input.modelId}`);\n }\n\n return buildContentRevalidationPaths({\n model,\n routeId: input.routeId,\n previousRouteId: input.previousRouteId,\n includeApi: input.includeApi,\n });\n}\n","// packages/nextion/src/content/search.ts\n//\n// Generic text-search helpers for content sources.\n\nexport function normalizeSearchQuery(query: string | null | undefined) {\n return String(query ?? \"\")\n .normalize(\"NFKC\")\n .trim()\n .replace(/\\s+/g, \" \")\n .toLowerCase();\n}\n\nfunction searchTerms(query: string | null | undefined) {\n const normalized = normalizeSearchQuery(query);\n return normalized ? normalized.split(\" \") : [];\n}\n\nfunction searchableText(values: readonly unknown[]) {\n return values\n .flatMap((value) => (Array.isArray(value) ? value : [value]))\n .filter((value): value is string | number | boolean =>\n [\"string\", \"number\", \"boolean\"].includes(typeof value)\n )\n .map((value) => String(value))\n .join(\" \")\n .normalize(\"NFKC\")\n .toLowerCase();\n}\n\nexport function matchesSearchQuery(\n values: readonly unknown[],\n query: string | null | undefined\n) {\n const terms = searchTerms(query);\n if (terms.length === 0) return true;\n\n const haystack = searchableText(values);\n return terms.every((term) => haystack.includes(term));\n}\n\nexport function filterItemsBySearch<TItem>(\n items: readonly TItem[],\n valuesForItem: (item: TItem) => readonly unknown[],\n query: string | null | undefined\n) {\n return items.filter((item) => matchesSearchQuery(valuesForItem(item), query));\n}\n","// packages/nextion/src/content/search-index.ts\n//\n// Generic D1-backed content search index helpers.\n\nimport type { SqlDatabaseAdapter } from \"../platform/runtime\";\nimport { matchesSearchQuery, normalizeSearchQuery } from \"./search\";\n\nexport type SearchIndexedItem = {\n pageId?: string;\n slug?: string;\n routeId?: string;\n title: string;\n description?: string;\n date?: string;\n author?: string;\n tags?: readonly string[];\n releaseDate?: string;\n director?: string;\n actors?: string;\n summary?: string;\n genres?: readonly string[];\n};\n\nexport type SearchIndexDocument = {\n modelId: string;\n pageId: string;\n routeId: string;\n title: string;\n summary: string;\n bodyText: string;\n facets: readonly string[];\n sourceUpdatedAt?: string | null;\n};\n\ntype SearchIndexRow = {\n route_id: string;\n};\n\ntype MaybePromise<T> = T | Promise<T>;\n\nfunction indexValuesForItem(item: SearchIndexedItem) {\n return [\n item.title,\n item.description,\n item.author,\n item.tags,\n item.slug,\n item.date,\n item.summary,\n item.director,\n item.actors,\n item.genres,\n item.routeId,\n item.releaseDate,\n ];\n}\n\nfunction routeIdForItem(item: SearchIndexedItem) {\n return item.routeId ?? item.slug ?? \"\";\n}\n\nfunction routeOrder<T extends SearchIndexedItem>(items: readonly T[]) {\n const order = new Map<string, number>();\n items.forEach((item, index) => {\n const routeId = routeIdForItem(item);\n if (routeId) order.set(routeId, index);\n });\n return order;\n}\n\nfunction uniqueValues(values: readonly string[]) {\n return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));\n}\n\nexport async function upsertSearchIndexDocument(\n db: SqlDatabaseAdapter,\n document: SearchIndexDocument\n) {\n const normalizedText = [\n document.title,\n document.summary,\n document.bodyText,\n ...document.facets,\n ]\n .join(\" \")\n .normalize(\"NFKC\")\n .replace(/\\s+/g, \" \")\n .toLowerCase();\n\n await db\n .prepare(\n `INSERT INTO content_search_index (\n model_id,\n page_id,\n route_id,\n title,\n summary,\n body_text,\n facets,\n normalized_text,\n source_updated_at,\n indexed_at\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))\n ON CONFLICT(model_id, route_id) DO UPDATE SET\n page_id = excluded.page_id,\n title = excluded.title,\n summary = excluded.summary,\n body_text = excluded.body_text,\n facets = excluded.facets,\n normalized_text = excluded.normalized_text,\n source_updated_at = excluded.source_updated_at,\n indexed_at = excluded.indexed_at`\n )\n .bind(\n document.modelId,\n document.pageId,\n document.routeId,\n document.title,\n document.summary,\n document.bodyText,\n JSON.stringify(uniqueValues([...document.facets])),\n normalizedText,\n document.sourceUpdatedAt ?? null\n )\n .run();\n}\n\nexport async function deleteSearchIndexDocument(\n db: SqlDatabaseAdapter,\n input: { modelId: string; routeId: string }\n) {\n await db\n .prepare(\n \"DELETE FROM content_search_index WHERE model_id = ? AND route_id = ?\"\n )\n .bind(input.modelId, input.routeId)\n .run();\n}\n\nexport async function deleteSearchIndexForModel(\n db: SqlDatabaseAdapter,\n input: { modelId: string }\n) {\n await db\n .prepare(\"DELETE FROM content_search_index WHERE model_id = ?\")\n .bind(input.modelId)\n .run();\n}\n\nexport async function getMissingSearchIndexRouteIds(\n db: SqlDatabaseAdapter,\n input: { modelId: string; routeIds: readonly string[] }\n) {\n const routeIds = uniqueValues([...input.routeIds]);\n if (routeIds.length === 0) return [];\n\n const placeholders = routeIds.map(() => \"?\").join(\", \");\n const result = await db\n .prepare(\n `SELECT route_id\n FROM content_search_index\n WHERE model_id = ? AND route_id IN (${placeholders})`\n )\n .bind(input.modelId, ...routeIds)\n .all<SearchIndexRow>();\n\n const present = new Set((result.results ?? []).map((row) => row.route_id));\n return routeIds.filter((routeId) => !present.has(routeId));\n}\n\nexport async function querySearchIndexRouteIds(\n db: SqlDatabaseAdapter,\n input: { modelId: string; query: string; limit?: number }\n) {\n const query = normalizeSearchQuery(input.query);\n if (!query) return [];\n\n const terms = query\n .split(\" \")\n .map((term) => `%${term}%`);\n const clauses = terms\n .map(\n () =>\n \"(normalized_text LIKE ? OR title LIKE ? OR summary LIKE ? OR facets LIKE ?)\"\n )\n .join(\" AND \");\n const values = terms.flatMap((term) => [term, term, term, term]);\n\n const result = await db\n .prepare(\n `SELECT route_id\n FROM content_search_index\n WHERE model_id = ? AND ${clauses}\n ORDER BY indexed_at DESC\n LIMIT ?`\n )\n .bind(input.modelId, ...values, input.limit ?? 200)\n .all<SearchIndexRow>();\n\n return (result.results ?? [])\n .map((row) => row.route_id)\n .filter((routeId): routeId is string => typeof routeId === \"string\");\n}\n\nexport async function filterItemsBySearchIndex<T extends SearchIndexedItem>(\n items: readonly T[],\n query: string | null | undefined,\n input: {\n modelId: string;\n filterFallback: (items: readonly T[], query: string | null | undefined) => T[];\n getDatabase?: () => MaybePromise<SqlDatabaseAdapter | null>;\n }\n) {\n const normalized = normalizeSearchQuery(query);\n if (!normalized) return [...items];\n if (!input.getDatabase) return input.filterFallback(items, normalized);\n\n try {\n const db = await input.getDatabase();\n if (!db) return input.filterFallback(items, normalized);\n const routeIds = await querySearchIndexRouteIds(db, {\n modelId: input.modelId,\n query: normalized,\n limit: Math.max(items.length, 200),\n });\n if (routeIds.length === 0) return input.filterFallback(items, normalized);\n\n const order = routeOrder(items);\n const matched = new Set(routeIds);\n return items\n .filter((item) => matched.has(routeIdForItem(item)))\n .sort(\n (a, b) =>\n (order.get(routeIdForItem(a)) ?? 0) -\n (order.get(routeIdForItem(b)) ?? 0)\n );\n } catch (error) {\n console.warn(\n JSON.stringify({\n tag: \"content_search_index_error\",\n modelId: input.modelId,\n message: error instanceof Error ? error.message : String(error),\n })\n );\n return input.filterFallback(items, normalized);\n }\n}\n\nexport function matchesIndexedItem(\n item: SearchIndexedItem,\n query: string | null | undefined\n) {\n return matchesSearchQuery(indexValuesForItem(item), query);\n}\n","// packages/nextion/src/content/prewarm.ts\n//\n// Generic content search index prewarming. The function takes a list\n// of (modelId, runner) targets and runs them. The starter wires the\n// project-specific runners in its `lib/content/prewarm.ts` shim.\n\nexport type ContentPrewarmModelResult = {\n modelId: string;\n ok: boolean;\n total: number;\n indexed: number;\n skipped: boolean;\n error?: string;\n};\n\nexport type ContentPrewarmResult = {\n ok: boolean;\n startedAt: string;\n finishedAt: string;\n durationMs: number;\n models: ContentPrewarmModelResult[];\n};\n\nexport type PrewarmTarget = {\n modelId: string;\n run: () => Promise<{ total: number; indexed: number; skipped: boolean }>;\n};\n\nfunction logContentPrewarm(fields: Record<string, unknown>) {\n try {\n console.log(JSON.stringify({ tag: \"content_prewarm\", ...fields }));\n } catch {\n // Ignore logging serialization errors.\n }\n}\n\nexport async function prewarmPublicContentSearchIndex(\n targets: readonly PrewarmTarget[],\n options?: { models?: readonly string[] }\n): Promise<ContentPrewarmResult> {\n const startedAtDate = new Date();\n const startedAt = startedAtDate.toISOString();\n const t0 = performance.now();\n const requestedModels = new Set(options?.models?.filter(Boolean));\n const selected =\n requestedModels.size > 0\n ? targets.filter((target) => requestedModels.has(target.modelId))\n : targets;\n\n const models: ContentPrewarmModelResult[] = [];\n for (const target of selected) {\n try {\n const result = await target.run();\n models.push({\n modelId: target.modelId,\n ok: true,\n total: result.total,\n indexed: result.indexed,\n skipped: result.skipped,\n });\n } catch (error) {\n models.push({\n modelId: target.modelId,\n ok: false,\n total: 0,\n indexed: 0,\n skipped: true,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n const finishedAt = new Date().toISOString();\n const output: ContentPrewarmResult = {\n ok: models.every((model) => model.ok),\n startedAt,\n finishedAt,\n durationMs: Math.round((performance.now() - t0) * 100) / 100,\n models,\n };\n\n logContentPrewarm({\n ok: output.ok,\n started_at: output.startedAt,\n finished_at: output.finishedAt,\n duration_ms: output.durationMs,\n models: output.models,\n });\n\n return output;\n}\n","type PropertyMap = Record<string, unknown>;\n\ntype TextPart = {\n plain_text?: string;\n};\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value && typeof value === \"object\");\n}\n\nfunction getPlainText(parts: unknown): string {\n if (!Array.isArray(parts)) return \"\";\n return parts\n .map((part: TextPart) => part.plain_text ?? \"\")\n .join(\"\")\n .trim();\n}\n\nfunction getProperty(properties: PropertyMap, key: string) {\n return properties[key] as Record<string, unknown> | undefined;\n}\n\nfunction firstPropertyOfType(properties: PropertyMap, type: string) {\n return Object.values(properties).find(\n (property): property is Record<string, unknown> =>\n isRecord(property) && property.type === type\n );\n}\n\nexport function getFirstTitleProperty(properties: PropertyMap): string {\n const property = firstPropertyOfType(properties, \"title\");\n return property ? getPlainText(property.title) : \"\";\n}\n\nexport function getRichTextProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (!property) return \"\";\n\n if (property.type === \"title\") return getPlainText(property.title);\n if (property.type === \"rich_text\") return getPlainText(property.rich_text);\n if (property.type === \"url\") return String(property.url ?? \"\").trim();\n if (property.type === \"email\") return String(property.email ?? \"\").trim();\n if (property.type === \"phone_number\") {\n return String(property.phone_number ?? \"\").trim();\n }\n\n return \"\";\n}\n\nexport function getDateProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (property?.type !== \"date\") return \"\";\n const date = property.date as { start?: string } | null | undefined;\n return String(date?.start ?? \"\").trim();\n}\n\nexport function getFirstDateProperty(properties: PropertyMap): string {\n const property = firstPropertyOfType(properties, \"date\");\n if (!property) return \"\";\n const date = property.date as { start?: string } | null | undefined;\n return String(date?.start ?? \"\").trim();\n}\n\nexport function getSelectProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (property?.type !== \"select\") return \"\";\n const select = property.select as { name?: string } | null | undefined;\n return String(select?.name ?? \"\").trim();\n}\n\nexport function getCheckboxProperty(properties: PropertyMap, key: string): boolean {\n const property = getProperty(properties, key);\n if (property?.type !== \"checkbox\") return false;\n return Boolean(property.checkbox);\n}\n\nexport function getNumberProperty(\n properties: PropertyMap,\n key: string,\n fallback = 0\n): number {\n const property = getProperty(properties, key);\n if (property?.type !== \"number\") return fallback;\n const value = Number(property.number);\n return Number.isFinite(value) ? value : fallback;\n}\n\nexport function getRelationPageIds(properties: PropertyMap, key: string): string[] {\n const property = getProperty(properties, key);\n if (property?.type !== \"relation\" || !Array.isArray(property.relation)) {\n return [];\n }\n\n return property.relation\n .map((item: { id?: string }) => String(item.id ?? \"\").trim())\n .filter(Boolean);\n}\n\nexport function getTagsProperty(properties: PropertyMap, key: string): string[] {\n const property = getProperty(properties, key);\n if (property?.type === \"multi_select\" && Array.isArray(property.multi_select)) {\n return property.multi_select\n .map((item: { name?: string }) => String(item.name ?? \"\").trim())\n .filter(Boolean);\n }\n\n if (property?.type === \"select\") {\n const select = property.select as { name?: string } | null | undefined;\n const name = String(select?.name ?? \"\").trim();\n return name ? [name] : [];\n }\n\n return [];\n}\n\nexport function getFirstTagsProperty(properties: PropertyMap): string[] {\n const multiSelect = firstPropertyOfType(properties, \"multi_select\");\n if (multiSelect && Array.isArray(multiSelect.multi_select)) {\n return multiSelect.multi_select\n .map((item: { name?: string }) => String(item.name ?? \"\").trim())\n .filter(Boolean);\n }\n\n const select = firstPropertyOfType(properties, \"select\");\n const name = String((select?.select as { name?: string } | null)?.name ?? \"\").trim();\n return name ? [name] : [];\n}\n\nexport function getAuthorProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (!property) return \"\";\n\n if (property.type === \"people\" && Array.isArray(property.people)) {\n return property.people\n .map((person: { name?: string; person?: { email?: string } }) =>\n String(person.name ?? person.person?.email ?? \"\").trim()\n )\n .filter(Boolean)\n .join(\", \");\n }\n\n return getRichTextProperty(properties, key);\n}\n\nexport function getFirstPeopleProperty(properties: PropertyMap): string {\n const property = firstPropertyOfType(properties, \"people\");\n if (!property || !Array.isArray(property.people)) return \"\";\n\n return property.people\n .map((person: { name?: string; person?: { email?: string } }) =>\n String(person.name ?? person.person?.email ?? \"\").trim()\n )\n .filter(Boolean)\n .join(\", \");\n}\n\nexport function pickPublishedFlag(properties: PropertyMap): boolean {\n const published = getProperty(properties, \"Published\");\n if (published?.type === \"checkbox\") {\n return Boolean(published.checkbox);\n }\n\n const status = getProperty(properties, \"Status\");\n if (status?.type === \"status\") {\n const statusValue = status.status as { name?: string } | null | undefined;\n return String(statusValue?.name ?? \"\").trim().toLowerCase() === \"published\";\n }\n\n if (status?.type === \"select\") {\n const statusValue = status.select as { name?: string } | null | undefined;\n return String(statusValue?.name ?? \"\").trim().toLowerCase() === \"published\";\n }\n\n return false;\n}\n\nexport function pickDescriptionFallback(description: string, title: string): string {\n return description.trim() || title.trim();\n}\n\nexport function isValidPublicSlug(slug: string): boolean {\n return /^[a-z0-9][a-z0-9-]{0,79}$/.test(slug);\n}\n\nexport function notionPageEditUrl(pageId: string, editBaseUrl?: string): string {\n const compactPageId = pageId.replaceAll(\"-\", \"\");\n if (editBaseUrl?.includes(\"{pageId}\")) {\n return editBaseUrl.replaceAll(\"{pageId}\", compactPageId);\n }\n return `https://www.notion.so/${compactPageId}`;\n}\n\n/**\n * Normalize a Notion page id (with or without dashes) to a compact lowercase\n * string. Used as a stable identifier in URLs and cache keys.\n */\nexport function compactNotionId(id: string): string {\n return id.replaceAll(\"-\", \"\").toLowerCase();\n}\n","import {\n compactNotionId,\n getCheckboxProperty,\n getRelationPageIds,\n getRichTextProperty,\n getSelectProperty,\n getTagsProperty,\n isRecord,\n isValidPublicSlug,\n notionPageEditUrl,\n} from \"../notion/property-mappers\";\nimport type { NotionPageLike } from \"../notion/types\";\n\nexport type LocalizedContentFields = {\n title: string;\n source: string;\n locale: string;\n slug: string;\n published: string;\n seoTitle?: string;\n seoDescription?: string;\n};\n\nexport type LocalizedContentExtraFieldKind =\n | \"text\"\n | \"tags\"\n | \"select\"\n | \"checkbox\";\n\nexport type LocalizedContentExtraFields = Record<\n string,\n string | { field: string; kind?: LocalizedContentExtraFieldKind }\n>;\n\nexport type LocalizedContentExtraValue = string | string[] | boolean;\n\nexport type LocalizedContentTranslationBase = {\n pageId: string;\n updatedAt?: string;\n sourcePageId: string;\n locale: string;\n slug: string;\n title: string;\n seoTitle: string;\n seoDescription: string;\n published: boolean;\n editUrl: string | null;\n sourceUrl: string | null;\n};\n\nexport type LocalizedContentTranslation<TExtra extends object = object> =\n LocalizedContentTranslationBase & TExtra;\n\nfunction readPublishedFlag(properties: Record<string, unknown>, field: string) {\n const property = properties[field] as Record<string, unknown> | undefined;\n if (property?.type === \"checkbox\") return getCheckboxProperty(properties, field);\n\n if (property?.type === \"status\") {\n const status = property.status as { name?: string } | null | undefined;\n return String(status?.name ?? \"\").trim().toLowerCase() === \"published\";\n }\n\n if (property?.type === \"select\") {\n const select = property.select as { name?: string } | null | undefined;\n return String(select?.name ?? \"\").trim().toLowerCase() === \"published\";\n }\n\n return false;\n}\n\nfunction readLocale(properties: Record<string, unknown>, field: string) {\n return (\n getSelectProperty(properties, field) || getRichTextProperty(properties, field)\n );\n}\n\nfunction normalizeExtraField(\n definition: string | { field: string; kind?: LocalizedContentExtraFieldKind }\n) {\n if (typeof definition === \"string\") return { field: definition, kind: \"text\" };\n return { field: definition.field, kind: definition.kind ?? \"text\" };\n}\n\nfunction readExtraFields<TExtra extends object>(\n properties: Record<string, unknown>,\n fields?: LocalizedContentExtraFields\n): TExtra {\n const result: Record<string, LocalizedContentExtraValue> = {};\n\n for (const [key, definition] of Object.entries(fields ?? {})) {\n const { field, kind } = normalizeExtraField(definition);\n if (kind === \"tags\") result[key] = getTagsProperty(properties, field);\n else if (kind === \"select\") result[key] = getSelectProperty(properties, field);\n else if (kind === \"checkbox\") result[key] = getCheckboxProperty(properties, field);\n else result[key] = getRichTextProperty(properties, field);\n }\n\n return result as TExtra;\n}\n\nexport function mapNotionPageToLocalizedContentTranslation<\n TExtra extends object = object,\n>(\n page: NotionPageLike,\n input: {\n fields: LocalizedContentFields;\n extraFields?: LocalizedContentExtraFields;\n editBaseUrl?: string;\n isValidLocale?: (locale: string) => boolean;\n }\n): LocalizedContentTranslation<TExtra> | null {\n const properties = isRecord(page.properties) ? page.properties : {};\n const sourcePageIds = getRelationPageIds(properties, input.fields.source);\n const locale = readLocale(properties, input.fields.locale);\n const configuredSlug = getRichTextProperty(\n properties,\n input.fields.slug\n ).toLowerCase();\n const slug = isValidPublicSlug(configuredSlug) ? configuredSlug : \"\";\n const title = getRichTextProperty(properties, input.fields.title);\n const published = readPublishedFlag(properties, input.fields.published);\n const sourceUrl =\n typeof page.public_url === \"string\" && page.public_url\n ? page.public_url\n : typeof page.url === \"string\"\n ? page.url\n : null;\n\n if (\n !sourcePageIds[0] ||\n !locale ||\n !slug ||\n !title ||\n !published ||\n (input.isValidLocale && !input.isValidLocale(locale))\n ) {\n return null;\n }\n\n return {\n pageId: page.id,\n ...(page.last_edited_time ? { updatedAt: page.last_edited_time } : {}),\n sourcePageId: sourcePageIds[0],\n locale,\n slug,\n title,\n seoTitle: input.fields.seoTitle\n ? getRichTextProperty(properties, input.fields.seoTitle)\n : \"\",\n seoDescription: input.fields.seoDescription\n ? getRichTextProperty(properties, input.fields.seoDescription)\n : \"\",\n published,\n editUrl: notionPageEditUrl(page.id, input.editBaseUrl),\n sourceUrl,\n ...readExtraFields<TExtra>(properties, input.extraFields),\n };\n}\n\nexport function localizeContentList<TBase, TTranslation, TResult>(input: {\n baseItems: readonly TBase[];\n translations: readonly TTranslation[];\n locale: string;\n defaultLocale: string;\n getBasePageId: (item: TBase) => string;\n getTranslationLocale: (translation: TTranslation) => string;\n getTranslationSourcePageId: (translation: TTranslation) => string;\n applyTranslation: (base: TBase, translation: TTranslation) => TResult | null;\n fallback: (base: TBase) => TResult;\n sort?: (left: TResult, right: TResult) => number;\n}) {\n const translationsForLocale = input.translations.filter(\n (translation) => input.getTranslationLocale(translation) === input.locale\n );\n\n if (translationsForLocale.length === 0 && input.locale === input.defaultLocale) {\n const fallback = input.baseItems.map(input.fallback);\n return input.sort ? fallback.sort(input.sort) : fallback;\n }\n\n const baseByPageId = new Map(\n input.baseItems.map((item) => [compactNotionId(input.getBasePageId(item)), item])\n );\n const localized = translationsForLocale\n .map((translation) => {\n const base = baseByPageId.get(\n compactNotionId(input.getTranslationSourcePageId(translation))\n );\n return base ? input.applyTranslation(base, translation) : null;\n })\n .filter((item): item is TResult => Boolean(item));\n\n return input.sort ? localized.sort(input.sort) : localized;\n}\n\nexport function getAlternateLocalizedContentLinks<TTranslation>(input: {\n translations: readonly TTranslation[];\n sourcePageId: string;\n currentLocale: string;\n getTranslationLocale: (translation: TTranslation) => string;\n getTranslationSlug: (translation: TTranslation) => string;\n getTranslationSourcePageId: (translation: TTranslation) => string;\n isValidLocale?: (locale: string) => boolean;\n hrefForTranslation: (locale: string, slug: string) => string;\n labelForLocale?: (locale: string) => string;\n}) {\n const normalizedSourcePageId = compactNotionId(input.sourcePageId);\n\n return input.translations\n .filter((translation) => {\n const locale = input.getTranslationLocale(translation);\n return (\n compactNotionId(input.getTranslationSourcePageId(translation)) ===\n normalizedSourcePageId &&\n locale !== input.currentLocale &&\n (!input.isValidLocale || input.isValidLocale(locale))\n );\n })\n .map((translation) => {\n const locale = input.getTranslationLocale(translation);\n const slug = input.getTranslationSlug(translation);\n return {\n locale,\n slug,\n href: input.hrefForTranslation(locale, slug),\n label: input.labelForLocale?.(locale) ?? locale,\n };\n });\n}\n","// packages/nextion/src/content/admin-summary.ts\n//\n// Generic admin summary helpers for content sources.\n\nimport type {\n ContentModelDefinition,\n NotionFieldMap,\n} from \"./models\";\nimport { getRegisteredSources } from \"./models\";\n\nexport type ContentModelAdminSummary = {\n id: string;\n name: string;\n kind: ContentModelDefinition[\"kind\"];\n visibility: \"public\" | \"admin\" | \"public+admin\" | \"private\";\n listPath: string;\n detailPath: string;\n publicApiPath?: string;\n dataSourceEnv: string;\n hasDefaultDataSource: boolean;\n fieldCount: number;\n capabilities: {\n richBlocks: boolean;\n coverImages: boolean;\n gatedAssets: boolean;\n };\n};\n\nfunction visibilityFor(model: ContentModelDefinition<NotionFieldMap>) {\n if (model.visibility.public && model.visibility.admin) return \"public+admin\";\n if (model.visibility.public) return \"public\";\n if (model.visibility.admin) return \"admin\";\n return \"private\";\n}\n\nexport function summarizeContentModelForAdmin(\n model: ContentModelDefinition<NotionFieldMap>\n): ContentModelAdminSummary {\n return {\n id: model.id,\n name: model.ui.name,\n kind: model.kind,\n visibility: visibilityFor(model),\n listPath: model.routes.listPath,\n detailPath: model.routes.detailPath,\n publicApiPath: model.routes.publicApiPath,\n dataSourceEnv: model.source.dataSourceEnv,\n hasDefaultDataSource: Boolean(model.source.defaultDataSourceId),\n fieldCount: Object.keys(model.source.fields).length,\n capabilities: model.capabilities,\n };\n}\n\nexport function getContentModelAdminSummaries(\n models: readonly ContentModelDefinition<NotionFieldMap>[] = getRegisteredSources()\n) {\n return models.map(summarizeContentModelForAdmin);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,67 @@
1
+ import { NotionPageLike } from '../notion/types.js';
2
+
3
+ type LocalizedContentFields = {
4
+ title: string;
5
+ source: string;
6
+ locale: string;
7
+ slug: string;
8
+ published: string;
9
+ seoTitle?: string;
10
+ seoDescription?: string;
11
+ };
12
+ type LocalizedContentExtraFieldKind = "text" | "tags" | "select" | "checkbox";
13
+ type LocalizedContentExtraFields = Record<string, string | {
14
+ field: string;
15
+ kind?: LocalizedContentExtraFieldKind;
16
+ }>;
17
+ type LocalizedContentExtraValue = string | string[] | boolean;
18
+ type LocalizedContentTranslationBase = {
19
+ pageId: string;
20
+ updatedAt?: string;
21
+ sourcePageId: string;
22
+ locale: string;
23
+ slug: string;
24
+ title: string;
25
+ seoTitle: string;
26
+ seoDescription: string;
27
+ published: boolean;
28
+ editUrl: string | null;
29
+ sourceUrl: string | null;
30
+ };
31
+ type LocalizedContentTranslation<TExtra extends object = object> = LocalizedContentTranslationBase & TExtra;
32
+ declare function mapNotionPageToLocalizedContentTranslation<TExtra extends object = object>(page: NotionPageLike, input: {
33
+ fields: LocalizedContentFields;
34
+ extraFields?: LocalizedContentExtraFields;
35
+ editBaseUrl?: string;
36
+ isValidLocale?: (locale: string) => boolean;
37
+ }): LocalizedContentTranslation<TExtra> | null;
38
+ declare function localizeContentList<TBase, TTranslation, TResult>(input: {
39
+ baseItems: readonly TBase[];
40
+ translations: readonly TTranslation[];
41
+ locale: string;
42
+ defaultLocale: string;
43
+ getBasePageId: (item: TBase) => string;
44
+ getTranslationLocale: (translation: TTranslation) => string;
45
+ getTranslationSourcePageId: (translation: TTranslation) => string;
46
+ applyTranslation: (base: TBase, translation: TTranslation) => TResult | null;
47
+ fallback: (base: TBase) => TResult;
48
+ sort?: (left: TResult, right: TResult) => number;
49
+ }): TResult[];
50
+ declare function getAlternateLocalizedContentLinks<TTranslation>(input: {
51
+ translations: readonly TTranslation[];
52
+ sourcePageId: string;
53
+ currentLocale: string;
54
+ getTranslationLocale: (translation: TTranslation) => string;
55
+ getTranslationSlug: (translation: TTranslation) => string;
56
+ getTranslationSourcePageId: (translation: TTranslation) => string;
57
+ isValidLocale?: (locale: string) => boolean;
58
+ hrefForTranslation: (locale: string, slug: string) => string;
59
+ labelForLocale?: (locale: string) => string;
60
+ }): {
61
+ locale: string;
62
+ slug: string;
63
+ href: string;
64
+ label: string;
65
+ }[];
66
+
67
+ export { type LocalizedContentExtraFieldKind, type LocalizedContentExtraFields, type LocalizedContentExtraValue, type LocalizedContentFields, type LocalizedContentTranslation, type LocalizedContentTranslationBase, getAlternateLocalizedContentLinks, localizeContentList, mapNotionPageToLocalizedContentTranslation };