@notion-headless-cms/core 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,22 @@
1
+ import {
2
+ CMSError,
3
+ isCMSError,
4
+ isCMSErrorInNamespace
5
+ } from "./chunk-V6ML4QE5.js";
6
+ import {
7
+ mergeHooks,
8
+ mergeLoggers
9
+ } from "./chunk-4KGKWKKI.js";
10
+ import {
11
+ memoryCache,
12
+ memoryDocumentCache,
13
+ memoryImageCache
14
+ } from "./chunk-6LHROEPI.js";
15
+ import {
16
+ noopDocumentCache,
17
+ noopImageCache
18
+ } from "./chunk-6DG63XUF.js";
19
+
1
20
  // src/cache.ts
2
21
  async function sha256Hex(input) {
3
22
  const data = new TextEncoder().encode(input);
@@ -9,178 +28,6 @@ function isStale(cachedAt, ttlMs) {
9
28
  return Date.now() - cachedAt > ttlMs;
10
29
  }
11
30
 
12
- // src/cache/memory.ts
13
- var MemoryDocumentCache = class {
14
- name = "memory-document";
15
- list = null;
16
- items = /* @__PURE__ */ new Map();
17
- getList() {
18
- return Promise.resolve(this.list);
19
- }
20
- setList(data) {
21
- this.list = data;
22
- return Promise.resolve();
23
- }
24
- getItem(slug) {
25
- return Promise.resolve(this.items.get(slug) ?? null);
26
- }
27
- setItem(slug, data) {
28
- this.items.set(slug, data);
29
- return Promise.resolve();
30
- }
31
- async invalidate(scope) {
32
- if (scope === "all") {
33
- this.list = null;
34
- this.items.clear();
35
- } else if ("slug" in scope) {
36
- this.items.delete(scope.slug);
37
- }
38
- }
39
- };
40
- var MemoryImageCache = class {
41
- name = "memory-image";
42
- store = /* @__PURE__ */ new Map();
43
- get(hash) {
44
- return Promise.resolve(this.store.get(hash) ?? null);
45
- }
46
- set(hash, data, contentType) {
47
- this.store.set(hash, { data, contentType });
48
- return Promise.resolve();
49
- }
50
- };
51
- function memoryDocumentCache() {
52
- return new MemoryDocumentCache();
53
- }
54
- function memoryImageCache() {
55
- return new MemoryImageCache();
56
- }
57
- function memoryCache() {
58
- return new MemoryDocumentCache();
59
- }
60
-
61
- // src/cache/noop.ts
62
- var NoopDocumentCache = class {
63
- name = "noop-document";
64
- getList() {
65
- return Promise.resolve(null);
66
- }
67
- setList(_data) {
68
- return Promise.resolve();
69
- }
70
- getItem(_slug) {
71
- return Promise.resolve(null);
72
- }
73
- setItem(_slug, _data) {
74
- return Promise.resolve();
75
- }
76
- invalidate(_scope) {
77
- return Promise.resolve();
78
- }
79
- };
80
- var NoopImageCache = class {
81
- name = "noop-image";
82
- get(_hash) {
83
- return Promise.resolve(null);
84
- }
85
- set(_hash, _data, _contentType) {
86
- return Promise.resolve();
87
- }
88
- };
89
- var _noopDocument = new NoopDocumentCache();
90
- var _noopImage = new NoopImageCache();
91
- function noopDocumentCache() {
92
- return _noopDocument;
93
- }
94
- function noopImageCache() {
95
- return _noopImage;
96
- }
97
-
98
- // src/cms.ts
99
- import { renderMarkdown } from "@notion-headless-cms/renderer";
100
-
101
- // src/errors.ts
102
- var CMSError = class extends Error {
103
- code;
104
- cause;
105
- context;
106
- constructor(params) {
107
- super(params.message, { cause: params.cause });
108
- this.name = "CMSError";
109
- this.code = params.code;
110
- this.cause = params.cause;
111
- this.context = params.context;
112
- }
113
- };
114
- function isCMSError(error) {
115
- return error instanceof CMSError;
116
- }
117
-
118
- // src/hooks.ts
119
- function mergeHooks(plugins, directHooks) {
120
- const allHooks = [
121
- ...plugins.map((p) => p.hooks ?? {}),
122
- ...directHooks ? [directHooks] : []
123
- ];
124
- if (allHooks.length === 0) return {};
125
- return {
126
- beforeCache: buildPipeline(allHooks, "beforeCache"),
127
- afterRender: buildRenderPipeline(allHooks),
128
- onCacheHit: buildObserver(allHooks, "onCacheHit"),
129
- onCacheMiss: buildObserver(allHooks, "onCacheMiss"),
130
- onListCacheHit: buildObserver(allHooks, "onListCacheHit"),
131
- onListCacheMiss: buildObserver(allHooks, "onListCacheMiss"),
132
- onError: buildObserver(allHooks, "onError")
133
- };
134
- }
135
- function buildPipeline(hooks, key) {
136
- const fns = hooks.map((h) => h[key]).filter(Boolean);
137
- if (fns.length === 0) return void 0;
138
- return async (item) => {
139
- let current = item;
140
- for (const fn of fns) {
141
- current = await fn(current);
142
- }
143
- return current;
144
- };
145
- }
146
- function buildRenderPipeline(hooks) {
147
- const fns = hooks.map((h) => h.afterRender).filter(Boolean);
148
- if (fns.length === 0) return void 0;
149
- return async (html, item) => {
150
- let current = html;
151
- for (const fn of fns) {
152
- current = await fn(current, item);
153
- }
154
- return current;
155
- };
156
- }
157
- function buildObserver(hooks, key) {
158
- const fns = hooks.map((h) => h[key]).filter(Boolean);
159
- if (fns.length === 0) return void 0;
160
- return ((...args) => {
161
- for (const fn of fns) {
162
- fn(...args);
163
- }
164
- });
165
- }
166
- function mergeLoggers(plugins, directLogger) {
167
- const loggers = [
168
- ...plugins.map((p) => p.logger ?? {}),
169
- ...directLogger ? [directLogger] : []
170
- ];
171
- if (loggers.length === 0) return void 0;
172
- const merged = {};
173
- for (const level of ["debug", "info", "warn", "error"]) {
174
- const fns = loggers.map((l) => l[level]).filter(Boolean);
175
- if (fns.length > 0) {
176
- merged[level] = (message, context) => {
177
- for (const fn of fns) fn(message, context);
178
- };
179
- }
180
- }
181
- return merged;
182
- }
183
-
184
31
  // src/image.ts
185
32
  function inferContentType(url, responseContentType) {
186
33
  if (responseContentType?.startsWith("image/")) {
@@ -200,7 +47,17 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
200
47
  const response = await fetch(notionUrl, {
201
48
  signal: AbortSignal.timeout(1e4)
202
49
  });
203
- if (!response.ok) return proxyUrl;
50
+ if (!response.ok) {
51
+ throw new CMSError({
52
+ code: "cache/image_fetch_failed",
53
+ message: `Failed to fetch Notion image: HTTP ${response.status}`,
54
+ context: {
55
+ operation: "fetchAndCacheImage",
56
+ notionUrl,
57
+ httpStatus: response.status
58
+ }
59
+ });
60
+ }
204
61
  const data = await response.arrayBuffer();
205
62
  const contentType = inferContentType(
206
63
  notionUrl,
@@ -208,8 +65,9 @@ async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
208
65
  );
209
66
  await cache.set(hash, data, contentType);
210
67
  } catch (err) {
68
+ if (isCMSError(err)) throw err;
211
69
  throw new CMSError({
212
- code: "IMAGE_CACHE_FAILED",
70
+ code: "cache/io_failed",
213
71
  message: "Failed to fetch or cache Notion image.",
214
72
  cause: err,
215
73
  context: { operation: "fetchAndCacheImage", notionUrl }
@@ -318,9 +176,20 @@ var QueryBuilder = class {
318
176
  const result = await this.execute();
319
177
  return result.items[0] ?? null;
320
178
  }
179
+ /** 前後アイテムを返す。sortBy() で指定したソート順を適用する。 */
321
180
  async adjacent(slug) {
322
181
  const statuses = this._statuses.length > 0 ? this._statuses : this.defaultStatuses.length > 0 ? this.defaultStatuses : void 0;
323
- const items = await this.source.list({ publishedStatuses: statuses });
182
+ let items = await this.source.list({ publishedStatuses: statuses });
183
+ if (this._sortField) {
184
+ const field = this._sortField;
185
+ const dir = this._sortDir;
186
+ items = [...items].sort((a, b) => {
187
+ const av = a[field];
188
+ const bv = b[field];
189
+ const cmp = av < bv ? -1 : av > bv ? 1 : 0;
190
+ return dir === "asc" ? cmp : -cmp;
191
+ });
192
+ }
324
193
  const idx = items.findIndex((item) => item.slug === slug);
325
194
  if (idx === -1) return { prev: null, next: null };
326
195
  return {
@@ -328,14 +197,18 @@ var QueryBuilder = class {
328
197
  next: idx < items.length - 1 ? items[idx + 1] : null
329
198
  };
330
199
  }
200
+ /** 最初の 1 件を返す。`.paginate({ page: 1, perPage: 1 }).executeOne()` の短縮形。 */
201
+ first() {
202
+ return this.paginate({ page: 1, perPage: 1 }).executeOne();
203
+ }
331
204
  };
332
205
 
333
206
  // src/retry.ts
334
207
  var DEFAULT_RETRY_CONFIG = {
335
- maxConcurrent: 3,
336
208
  retryOn: [429, 502, 503],
337
209
  maxRetries: 4,
338
- baseDelayMs: 1e3
210
+ baseDelayMs: 1e3,
211
+ jitter: true
339
212
  };
340
213
  async function withRetry(fn, config) {
341
214
  let lastError;
@@ -350,9 +223,9 @@ async function withRetry(fn, config) {
350
223
  lastError = err;
351
224
  if (attempt < config.maxRetries) {
352
225
  config.onRetry?.(attempt + 1, status);
353
- await new Promise(
354
- (resolve) => setTimeout(resolve, config.baseDelayMs * 2 ** attempt)
355
- );
226
+ const jitterFactor = config.jitter !== false ? 0.5 + Math.random() * 0.5 : 1;
227
+ const delay = config.baseDelayMs * 2 ** attempt * jitterFactor;
228
+ await new Promise((resolve) => setTimeout(resolve, delay));
356
229
  }
357
230
  }
358
231
  }
@@ -362,17 +235,25 @@ async function withRetry(fn, config) {
362
235
  // src/cms.ts
363
236
  var DEFAULT_IMAGE_PROXY_BASE = "/api/images";
364
237
  function resolveDocumentCache(cache) {
365
- if (!cache || cache.document === false || cache.document === void 0) {
238
+ if (!cache || cache === "disabled" || !cache.document) {
366
239
  return noopDocumentCache();
367
240
  }
368
241
  return cache.document;
369
242
  }
370
243
  function resolveImageCache(cache) {
371
- if (!cache || cache.image === false || cache.image === void 0) {
244
+ if (!cache || cache === "disabled" || !cache.image) {
372
245
  return noopImageCache();
373
246
  }
374
247
  return cache.image;
375
248
  }
249
+ function resolveTtl(cache) {
250
+ if (!cache || cache === "disabled") return void 0;
251
+ return cache.ttlMs;
252
+ }
253
+ function hasImageCacheConfigured(cache) {
254
+ if (!cache || cache === "disabled") return false;
255
+ return !!cache.image;
256
+ }
376
257
  var CMS = class {
377
258
  source;
378
259
  docCache;
@@ -383,34 +264,35 @@ var CMS = class {
383
264
  accessibleStatuses;
384
265
  imageProxyBase;
385
266
  contentConfig;
267
+ rendererFn;
386
268
  waitUntil;
387
269
  hooks;
388
270
  logger;
389
271
  retryConfig;
390
- cached;
272
+ maxConcurrent;
391
273
  cache;
392
274
  constructor(opts) {
393
275
  this.source = opts.source;
394
276
  this.docCache = resolveDocumentCache(opts.cache);
395
277
  this.imgCache = resolveImageCache(opts.cache);
396
- this.hasImageCache = !!opts.cache?.image;
397
- this.ttlMs = opts.cache?.ttlMs;
278
+ this.hasImageCache = hasImageCacheConfigured(opts.cache);
279
+ this.ttlMs = resolveTtl(opts.cache);
398
280
  this.publishedStatuses = opts.schema?.publishedStatuses ?? (opts.source.publishedStatuses ? [...opts.source.publishedStatuses] : []);
399
281
  this.accessibleStatuses = opts.schema?.accessibleStatuses ?? (opts.source.accessibleStatuses ? [...opts.source.accessibleStatuses] : []);
400
282
  this.imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
401
283
  this.contentConfig = opts.content;
284
+ this.rendererFn = opts.renderer;
402
285
  this.waitUntil = opts.waitUntil;
403
- this.hooks = mergeHooks(opts.plugins ?? [], opts.hooks);
404
286
  this.logger = mergeLoggers(opts.plugins ?? [], opts.logger);
287
+ this.hooks = mergeHooks(opts.plugins ?? [], opts.hooks, this.logger);
288
+ this.maxConcurrent = opts.rateLimiter?.maxConcurrent ?? 3;
405
289
  this.retryConfig = {
406
290
  ...DEFAULT_RETRY_CONFIG,
407
- ...opts.rateLimiter
408
- };
409
- this.cached = {
410
- list: this.cachedList.bind(this),
411
- get: this.cachedGet.bind(this)
291
+ ...opts.rateLimiter ?? {}
412
292
  };
413
293
  this.cache = {
294
+ getList: this.cachedList.bind(this),
295
+ get: this.cachedGet.bind(this),
414
296
  prefetchAll: this.prefetchAll.bind(this),
415
297
  revalidate: this.revalidate.bind(this),
416
298
  sync: this.syncFromWebhook.bind(this),
@@ -442,21 +324,28 @@ var CMS = class {
442
324
  }
443
325
  });
444
326
  if (!item) return null;
445
- if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
327
+ if (this.accessibleStatuses.length > 0 && (!item.status || !this.accessibleStatuses.includes(item.status))) {
446
328
  return null;
447
329
  }
448
330
  return item;
449
331
  }
450
- /** @deprecated find() を使用してください。 */
451
- findBySlug(slug) {
452
- return this.find(slug);
332
+ /** 複数スラッグをまとめてソースから直接取得する。accessibleStatuses フィルタを適用する。 */
333
+ async findMany(slugs) {
334
+ const results = /* @__PURE__ */ new Map();
335
+ await Promise.all(
336
+ slugs.map(async (slug) => {
337
+ const item = await this.find(slug);
338
+ if (item) results.set(slug, item);
339
+ })
340
+ );
341
+ return results;
453
342
  }
454
343
  /** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
455
344
  isPublished(item) {
456
345
  if (this.publishedStatuses.length === 0) return true;
457
- return this.publishedStatuses.includes(item.status);
346
+ return !!item.status && this.publishedStatuses.includes(item.status);
458
347
  }
459
- /** コンテンツを Markdown → HTML にレンダリングし、CachedItem として返す。 */
348
+ /** コンテンツを Markdown → HTML にレンダリングし、CachedItem として返す。キャッシュには保存しない。 */
460
349
  async render(item) {
461
350
  return this.buildCachedItem(item);
462
351
  }
@@ -464,10 +353,65 @@ var CMS = class {
464
353
  query() {
465
354
  return new QueryBuilder(this.source, this.publishedStatuses);
466
355
  }
467
- /** 全コンテンツを事前レンダリングしてキャッシュに保存する。 */
356
+ /** 静的生成用のスラッグ一覧を返す。 */
357
+ async getStaticSlugs() {
358
+ const items = await this.list();
359
+ return items.map((item) => item.slug);
360
+ }
361
+ // ── 画像配信 ──────────────────────────────────────────────────────────
362
+ /** ハッシュキーでキャッシュ画像を取得する。 */
363
+ getCachedImage(hash) {
364
+ return this.imgCache.get(hash);
365
+ }
366
+ /** ハッシュキーでキャッシュ画像を Response として返す。 */
367
+ async createCachedImageResponse(hash) {
368
+ const object = await this.imgCache.get(hash);
369
+ if (!object) return null;
370
+ const headers = new Headers();
371
+ if (object.contentType) headers.set("content-type", object.contentType);
372
+ headers.set("cache-control", "public, max-age=31536000, immutable");
373
+ return new Response(object.data, { headers });
374
+ }
375
+ // ── キャッシュ優先取得(Stale-While-Revalidate) ─────────────────────
376
+ async cachedList() {
377
+ const cached = await this.docCache.getList();
378
+ if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
379
+ this.hooks.onListCacheHit?.(cached.items, cached.cachedAt);
380
+ return { items: cached.items, isStale: false, cachedAt: cached.cachedAt };
381
+ }
382
+ this.hooks.onListCacheMiss?.();
383
+ const items = await this.list();
384
+ const cachedAt = Date.now();
385
+ const save = this.docCache.setList({ items, cachedAt });
386
+ if (this.waitUntil) {
387
+ this.waitUntil(save);
388
+ } else {
389
+ await save;
390
+ }
391
+ return { items, isStale: !!cached, cachedAt };
392
+ }
393
+ async cachedGet(slug) {
394
+ const cached = await this.docCache.getItem(slug);
395
+ if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
396
+ this.hooks.onCacheHit?.(slug, cached);
397
+ return cached;
398
+ }
399
+ this.hooks.onCacheMiss?.(slug);
400
+ const item = await this.find(slug);
401
+ if (!item) return null;
402
+ const entry = await this.buildCachedItem(item);
403
+ const save = this.docCache.setItem(slug, entry);
404
+ if (this.waitUntil) {
405
+ this.waitUntil(save);
406
+ } else {
407
+ await save;
408
+ }
409
+ return entry;
410
+ }
411
+ // ── キャッシュ管理 ────────────────────────────────────────────────────
468
412
  async prefetchAll(opts) {
469
413
  const items = await this.list();
470
- const concurrency = opts?.concurrency ?? 3;
414
+ const concurrency = opts?.concurrency ?? this.maxConcurrent;
471
415
  let ok = 0;
472
416
  let failed = 0;
473
417
  for (let i = 0; i < items.length; i += concurrency) {
@@ -478,8 +422,16 @@ var CMS = class {
478
422
  const rendered = await this.buildCachedItem(item);
479
423
  await this.docCache.setItem(item.slug, rendered);
480
424
  ok++;
481
- } catch {
425
+ } catch (err) {
482
426
  failed++;
427
+ this.logger?.warn?.(
428
+ "prefetchAll: \u30A2\u30A4\u30C6\u30E0\u306E\u4E8B\u524D\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u306B\u5931\u6557",
429
+ {
430
+ slug: item.slug,
431
+ pageId: item.id,
432
+ error: err instanceof Error ? err.message : String(err)
433
+ }
434
+ );
483
435
  }
484
436
  })
485
437
  );
@@ -488,17 +440,10 @@ var CMS = class {
488
440
  await this.docCache.setList({ items, cachedAt: Date.now() });
489
441
  return { ok, failed };
490
442
  }
491
- /** 静的生成用のスラッグ一覧を返す。 */
492
- async getStaticSlugs() {
493
- const items = await this.list();
494
- return items.map((item) => item.slug);
495
- }
496
- /** 指定スコープのキャッシュを無効化する。 */
497
443
  async revalidate(scope) {
498
444
  if (!this.docCache.invalidate) return;
499
445
  await this.docCache.invalidate(scope ?? "all");
500
446
  }
501
- /** Webhook ペイロードを元にキャッシュを同期する。 */
502
447
  async syncFromWebhook(payload) {
503
448
  const updated = [];
504
449
  if (payload?.slug) {
@@ -517,44 +462,6 @@ var CMS = class {
517
462
  }
518
463
  return { updated };
519
464
  }
520
- // ── キャッシュ優先取得(Stale-While-Revalidate) ──────────────────────
521
- /** キャッシュ優先でコンテンツ一覧を返す(SWR)。 */
522
- async cachedList() {
523
- const cached = await this.docCache.getList();
524
- if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
525
- this.hooks.onListCacheHit?.(cached.items, cached.cachedAt);
526
- return { items: cached.items, isStale: false, cachedAt: cached.cachedAt };
527
- }
528
- this.hooks.onListCacheMiss?.();
529
- const items = await this.list();
530
- const cachedAt = Date.now();
531
- const save = this.docCache.setList({ items, cachedAt });
532
- if (this.waitUntil) {
533
- this.waitUntil(save);
534
- } else {
535
- await save;
536
- }
537
- return { items, isStale: !!cached, cachedAt };
538
- }
539
- /** キャッシュ優先で単一コンテンツを返す(SWR)。 */
540
- async cachedGet(slug) {
541
- const cached = await this.docCache.getItem(slug);
542
- if (cached && !isStale(cached.cachedAt, this.ttlMs)) {
543
- this.hooks.onCacheHit?.(slug, cached);
544
- return cached;
545
- }
546
- this.hooks.onCacheMiss?.(slug);
547
- const item = await this.find(slug);
548
- if (!item) return null;
549
- const entry = await this.buildCachedItem(item);
550
- const save = this.docCache.setItem(slug, entry);
551
- if (this.waitUntil) {
552
- this.waitUntil(save);
553
- } else {
554
- await save;
555
- }
556
- return entry;
557
- }
558
465
  async checkListUpdate(version) {
559
466
  const items = await this.list();
560
467
  const serverVersion = buildListVersion(items);
@@ -576,59 +483,6 @@ var CMS = class {
576
483
  notionUpdatedAt: entry.notionUpdatedAt
577
484
  };
578
485
  }
579
- // ── 後方互換 SWR ────────────────────────────────────────────────────────
580
- /** @deprecated cached.list() を使用してください。 */
581
- async getList() {
582
- const result = await this.cachedList();
583
- return { items: result.items, listVersion: buildListVersion(result.items) };
584
- }
585
- /** @deprecated cached.get() を使用してください。 */
586
- getItem(slug) {
587
- return this.cachedGet(slug);
588
- }
589
- /** @deprecated cache.prefetchAll() を使用してください。 */
590
- async prefetchAllLegacy(opts) {
591
- return this.prefetchAll(opts);
592
- }
593
- // ── 後方互換クエリ API ──────────────────────────────────────────────────
594
- /** @deprecated query().status(s).execute() を使用してください。 */
595
- async listByStatus(status) {
596
- const statuses = Array.isArray(status) ? status : [status];
597
- return this.query().status(statuses).execute().then((r) => r.items);
598
- }
599
- /** @deprecated query().where(pred).execute() を使用してください。 */
600
- async where(predicate) {
601
- return this.query().where(predicate).execute().then((r) => r.items);
602
- }
603
- /** @deprecated query().paginate(opts).execute() を使用してください。 */
604
- async paginate(opts) {
605
- const result = await this.query().paginate(opts).execute();
606
- return {
607
- items: result.items,
608
- total: result.total,
609
- page: result.page,
610
- perPage: result.perPage,
611
- hasNext: result.hasNext
612
- };
613
- }
614
- /** @deprecated query().adjacent(slug) を使用してください。 */
615
- getAdjacent(slug) {
616
- return this.query().adjacent(slug);
617
- }
618
- // ── 画像配信 ───────────────────────────────────────────────────────────
619
- /** ハッシュキーでキャッシュ画像を取得する。 */
620
- getCachedImage(hash) {
621
- return this.imgCache.get(hash);
622
- }
623
- /** ハッシュキーでキャッシュ画像を Response として返す。 */
624
- async createCachedImageResponse(hash) {
625
- const object = await this.imgCache.get(hash);
626
- if (!object) return null;
627
- const headers = new Headers();
628
- if (object.contentType) headers.set("content-type", object.contentType);
629
- headers.set("cache-control", "public, max-age=31536000, immutable");
630
- return new Response(object.data, { headers });
631
- }
632
486
  // ── プライベートヘルパー ────────────────────────────────────────────────
633
487
  async buildCachedItem(item) {
634
488
  const start = Date.now();
@@ -636,13 +490,14 @@ var CMS = class {
636
490
  slug: item.slug,
637
491
  pageId: item.id
638
492
  });
493
+ this.hooks.onRenderStart?.(item.slug);
639
494
  let markdown;
640
495
  try {
641
496
  markdown = await this.source.loadMarkdown(item);
642
497
  } catch (err) {
643
498
  if (isCMSError(err)) throw err;
644
499
  throw new CMSError({
645
- code: "NOTION_MARKDOWN_FETCH_FAILED",
500
+ code: "source/load_markdown_failed",
646
501
  message: "Failed to load markdown from source.",
647
502
  cause: err,
648
503
  context: {
@@ -654,18 +509,18 @@ var CMS = class {
654
509
  }
655
510
  const cacheImage = this.hasImageCache ? buildCacheImageFn(this.imgCache, this.imageProxyBase) : void 0;
656
511
  let html;
512
+ const rendererFn = this.rendererFn ?? await loadDefaultRenderer();
657
513
  try {
658
- html = await renderMarkdown(markdown, {
514
+ html = await rendererFn(markdown, {
659
515
  imageProxyBase: this.imageProxyBase,
660
516
  cacheImage,
661
517
  remarkPlugins: this.contentConfig?.remarkPlugins,
662
- rehypePlugins: this.contentConfig?.rehypePlugins,
663
- render: this.contentConfig?.render
518
+ rehypePlugins: this.contentConfig?.rehypePlugins
664
519
  });
665
520
  } catch (err) {
666
521
  if (isCMSError(err)) throw err;
667
522
  throw new CMSError({
668
- code: "RENDERER_FAILED",
523
+ code: "renderer/failed",
669
524
  message: "Failed to render markdown.",
670
525
  cause: err,
671
526
  context: {
@@ -687,13 +542,28 @@ var CMS = class {
687
542
  if (this.hooks.beforeCache) {
688
543
  result = await this.hooks.beforeCache(result);
689
544
  }
545
+ const durationMs = Date.now() - start;
690
546
  this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u5B8C\u4E86", {
691
547
  slug: item.slug,
692
- durationMs: Date.now() - start
548
+ durationMs
693
549
  });
550
+ this.hooks.onRenderEnd?.(item.slug, durationMs);
694
551
  return result;
695
552
  }
696
553
  };
554
+ async function loadDefaultRenderer() {
555
+ try {
556
+ const mod = await import("@notion-headless-cms/renderer");
557
+ return mod.renderMarkdown;
558
+ } catch (err) {
559
+ throw new CMSError({
560
+ code: "renderer/failed",
561
+ message: "renderer \u30AA\u30D7\u30B7\u30E7\u30F3\u304C\u672A\u6307\u5B9A\u3067 @notion-headless-cms/renderer \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002 createCMS({ renderer }) \u3067\u30EC\u30F3\u30C0\u30E9\u30FC\u3092\u6CE8\u5165\u3059\u308B\u304B\u3001@notion-headless-cms/renderer \u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
562
+ cause: err,
563
+ context: { operation: "loadDefaultRenderer" }
564
+ });
565
+ }
566
+ }
697
567
  function buildListVersion(items) {
698
568
  return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
699
569
  }
@@ -701,44 +571,6 @@ function createCMS(opts) {
701
571
  return new CMS(opts);
702
572
  }
703
573
 
704
- // src/mapper.ts
705
- import { z } from "zod";
706
- var baseContentItemSchema = z.object({
707
- id: z.string().min(1),
708
- slug: z.string(),
709
- status: z.string(),
710
- publishedAt: z.string().min(1),
711
- updatedAt: z.string().min(1)
712
- });
713
- function getPlainText(items) {
714
- return items?.map((item) => item.plain_text).join("") ?? "";
715
- }
716
- function mapItem(page, props) {
717
- const statusProperty = page.properties[props.status];
718
- const dateProperty = page.properties[props.date];
719
- const parsed = baseContentItemSchema.safeParse({
720
- id: page.id,
721
- slug: getPlainText(
722
- page.properties[props.slug]?.rich_text
723
- ),
724
- status: statusProperty?.status?.name ?? statusProperty?.select?.name ?? "",
725
- publishedAt: dateProperty?.date?.start ?? page.created_time,
726
- updatedAt: page.last_edited_time
727
- });
728
- if (!parsed.success) {
729
- throw new CMSError({
730
- code: "NOTION_ITEM_SCHEMA_INVALID",
731
- message: "Failed to parse Notion page into BaseContentItem.",
732
- context: {
733
- operation: "mapItem",
734
- pageId: page.id,
735
- issues: JSON.stringify(parsed.error.issues)
736
- }
737
- });
738
- }
739
- return parsed.data;
740
- }
741
-
742
574
  // src/types/plugin.ts
743
575
  function definePlugin(plugin) {
744
576
  return plugin;
@@ -750,10 +582,9 @@ export {
750
582
  QueryBuilder,
751
583
  createCMS,
752
584
  definePlugin,
753
- getPlainText,
754
585
  isCMSError,
586
+ isCMSErrorInNamespace,
755
587
  isStale,
756
- mapItem,
757
588
  memoryCache,
758
589
  memoryDocumentCache,
759
590
  memoryImageCache,
@@ -764,3 +595,4 @@ export {
764
595
  sha256Hex,
765
596
  withRetry
766
597
  };
598
+ //# sourceMappingURL=index.js.map