@notion-headless-cms/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.
package/dist/index.js DELETED
@@ -1,766 +0,0 @@
1
- // src/cache.ts
2
- async function sha256Hex(input) {
3
- const data = new TextEncoder().encode(input);
4
- const hash = await crypto.subtle.digest("SHA-256", data);
5
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
6
- }
7
- function isStale(cachedAt, ttlMs) {
8
- if (ttlMs === void 0) return false;
9
- return Date.now() - cachedAt > ttlMs;
10
- }
11
-
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
- // src/image.ts
185
- function inferContentType(url, responseContentType) {
186
- if (responseContentType?.startsWith("image/")) {
187
- return responseContentType.split(";")[0].trim();
188
- }
189
- if (url.includes(".png")) return "image/png";
190
- if (url.includes(".gif")) return "image/gif";
191
- if (url.includes(".webp")) return "image/webp";
192
- return "image/jpeg";
193
- }
194
- async function fetchAndCacheImage(cache, notionUrl, imageProxyBase) {
195
- const hash = await sha256Hex(notionUrl);
196
- const proxyUrl = `${imageProxyBase}/${hash}`;
197
- const existing = await cache.get(hash);
198
- if (existing) return proxyUrl;
199
- try {
200
- const response = await fetch(notionUrl, {
201
- signal: AbortSignal.timeout(1e4)
202
- });
203
- if (!response.ok) return proxyUrl;
204
- const data = await response.arrayBuffer();
205
- const contentType = inferContentType(
206
- notionUrl,
207
- response.headers.get("content-type")
208
- );
209
- await cache.set(hash, data, contentType);
210
- } catch (err) {
211
- throw new CMSError({
212
- code: "IMAGE_CACHE_FAILED",
213
- message: "Failed to fetch or cache Notion image.",
214
- cause: err,
215
- context: { operation: "fetchAndCacheImage", notionUrl }
216
- });
217
- }
218
- return proxyUrl;
219
- }
220
- function buildCacheImageFn(cache, imageProxyBase) {
221
- return (notionUrl) => fetchAndCacheImage(cache, notionUrl, imageProxyBase);
222
- }
223
-
224
- // src/query.ts
225
- var QueryBuilder = class {
226
- source;
227
- defaultStatuses;
228
- _statuses = [];
229
- _tags = [];
230
- _predicate;
231
- _sortField;
232
- _sortDir = "asc";
233
- _page = 1;
234
- _perPage = 20;
235
- constructor(source, defaultStatuses = []) {
236
- this.source = source;
237
- this.defaultStatuses = defaultStatuses;
238
- }
239
- status(s) {
240
- this._statuses = Array.isArray(s) ? s : [s];
241
- return this;
242
- }
243
- tag(t) {
244
- this._tags = Array.isArray(t) ? t : [t];
245
- return this;
246
- }
247
- where(predicate) {
248
- this._predicate = predicate;
249
- return this;
250
- }
251
- sortBy(field, dir = "asc") {
252
- this._sortField = field;
253
- this._sortDir = dir;
254
- return this;
255
- }
256
- paginate(opts) {
257
- this._page = opts.page;
258
- this._perPage = opts.perPage;
259
- return this;
260
- }
261
- async execute() {
262
- const statuses = this._statuses.length > 0 ? this._statuses : this.defaultStatuses.length > 0 ? this.defaultStatuses : void 0;
263
- if (this.source.query && !this._predicate) {
264
- const result = await this.source.query({
265
- filter: {
266
- statuses,
267
- tags: this._tags.length > 0 ? this._tags : void 0
268
- },
269
- sort: this._sortField ? [{ property: this._sortField, direction: this._sortDir }] : void 0,
270
- pageSize: this._perPage
271
- });
272
- const items2 = result.items;
273
- return {
274
- items: items2,
275
- total: items2.length,
276
- page: this._page,
277
- perPage: this._perPage,
278
- hasNext: result.hasMore,
279
- hasPrev: this._page > 1
280
- };
281
- }
282
- let items = await this.source.list({
283
- publishedStatuses: statuses
284
- });
285
- if (this._tags.length > 0) {
286
- items = items.filter((item) => {
287
- const itemTags = item.tags;
288
- if (!Array.isArray(itemTags)) return false;
289
- return this._tags.some((tag) => itemTags.includes(tag));
290
- });
291
- }
292
- if (this._predicate) {
293
- items = items.filter(this._predicate);
294
- }
295
- if (this._sortField) {
296
- const field = this._sortField;
297
- const dir = this._sortDir;
298
- items = [...items].sort((a, b) => {
299
- const av = a[field];
300
- const bv = b[field];
301
- const cmp = av < bv ? -1 : av > bv ? 1 : 0;
302
- return dir === "asc" ? cmp : -cmp;
303
- });
304
- }
305
- const total = items.length;
306
- const start = (this._page - 1) * this._perPage;
307
- const paged = items.slice(start, start + this._perPage);
308
- return {
309
- items: paged,
310
- total,
311
- page: this._page,
312
- perPage: this._perPage,
313
- hasNext: start + this._perPage < total,
314
- hasPrev: this._page > 1
315
- };
316
- }
317
- async executeOne() {
318
- const result = await this.execute();
319
- return result.items[0] ?? null;
320
- }
321
- async adjacent(slug) {
322
- 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 });
324
- const idx = items.findIndex((item) => item.slug === slug);
325
- if (idx === -1) return { prev: null, next: null };
326
- return {
327
- prev: idx > 0 ? items[idx - 1] : null,
328
- next: idx < items.length - 1 ? items[idx + 1] : null
329
- };
330
- }
331
- };
332
-
333
- // src/retry.ts
334
- var DEFAULT_RETRY_CONFIG = {
335
- maxConcurrent: 3,
336
- retryOn: [429, 502, 503],
337
- maxRetries: 4,
338
- baseDelayMs: 1e3
339
- };
340
- async function withRetry(fn, config) {
341
- let lastError;
342
- for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
343
- try {
344
- return await fn();
345
- } catch (err) {
346
- const status = err.status;
347
- if (status === void 0 || !config.retryOn.includes(status)) {
348
- throw err;
349
- }
350
- lastError = err;
351
- if (attempt < config.maxRetries) {
352
- config.onRetry?.(attempt + 1, status);
353
- await new Promise(
354
- (resolve) => setTimeout(resolve, config.baseDelayMs * 2 ** attempt)
355
- );
356
- }
357
- }
358
- }
359
- throw lastError;
360
- }
361
-
362
- // src/cms.ts
363
- var DEFAULT_IMAGE_PROXY_BASE = "/api/images";
364
- function resolveDocumentCache(cache) {
365
- if (!cache || cache.document === false || cache.document === void 0) {
366
- return noopDocumentCache();
367
- }
368
- return cache.document;
369
- }
370
- function resolveImageCache(cache) {
371
- if (!cache || cache.image === false || cache.image === void 0) {
372
- return noopImageCache();
373
- }
374
- return cache.image;
375
- }
376
- var CMS = class {
377
- source;
378
- docCache;
379
- imgCache;
380
- hasImageCache;
381
- ttlMs;
382
- publishedStatuses;
383
- accessibleStatuses;
384
- imageProxyBase;
385
- contentConfig;
386
- waitUntil;
387
- hooks;
388
- logger;
389
- retryConfig;
390
- cached;
391
- cache;
392
- constructor(opts) {
393
- this.source = opts.source;
394
- this.docCache = resolveDocumentCache(opts.cache);
395
- this.imgCache = resolveImageCache(opts.cache);
396
- this.hasImageCache = !!opts.cache?.image;
397
- this.ttlMs = opts.cache?.ttlMs;
398
- this.publishedStatuses = opts.schema?.publishedStatuses ?? (opts.source.publishedStatuses ? [...opts.source.publishedStatuses] : []);
399
- this.accessibleStatuses = opts.schema?.accessibleStatuses ?? (opts.source.accessibleStatuses ? [...opts.source.accessibleStatuses] : []);
400
- this.imageProxyBase = opts.content?.imageProxyBase ?? DEFAULT_IMAGE_PROXY_BASE;
401
- this.contentConfig = opts.content;
402
- this.waitUntil = opts.waitUntil;
403
- this.hooks = mergeHooks(opts.plugins ?? [], opts.hooks);
404
- this.logger = mergeLoggers(opts.plugins ?? [], opts.logger);
405
- this.retryConfig = {
406
- ...DEFAULT_RETRY_CONFIG,
407
- ...opts.rateLimiter
408
- };
409
- this.cached = {
410
- list: this.cachedList.bind(this),
411
- get: this.cachedGet.bind(this)
412
- };
413
- this.cache = {
414
- prefetchAll: this.prefetchAll.bind(this),
415
- revalidate: this.revalidate.bind(this),
416
- sync: this.syncFromWebhook.bind(this),
417
- checkList: this.checkListUpdate.bind(this),
418
- checkItem: this.checkItemUpdate.bind(this)
419
- };
420
- }
421
- // ── コンテンツ取得 ──────────────────────────────────────────────────────
422
- /** 公開済みコンテンツ一覧をソースから直接取得する。 */
423
- list() {
424
- return withRetry(
425
- () => this.source.list({
426
- publishedStatuses: this.publishedStatuses.length > 0 ? this.publishedStatuses : void 0
427
- }),
428
- {
429
- ...this.retryConfig,
430
- onRetry: (attempt, status) => {
431
- this.logger?.warn?.("list() \u30EA\u30C8\u30E9\u30A4\u4E2D", { attempt, status });
432
- }
433
- }
434
- );
435
- }
436
- /** スラッグでコンテンツをソースから直接取得する。 */
437
- async find(slug) {
438
- const item = await withRetry(() => this.source.findBySlug(slug), {
439
- ...this.retryConfig,
440
- onRetry: (attempt, status) => {
441
- this.logger?.warn?.("find() \u30EA\u30C8\u30E9\u30A4\u4E2D", { attempt, status, slug });
442
- }
443
- });
444
- if (!item) return null;
445
- if (this.accessibleStatuses.length > 0 && !this.accessibleStatuses.includes(item.status)) {
446
- return null;
447
- }
448
- return item;
449
- }
450
- /** @deprecated find() を使用してください。 */
451
- findBySlug(slug) {
452
- return this.find(slug);
453
- }
454
- /** アイテムが publishedStatuses に含まれるステータスかどうかを返す。 */
455
- isPublished(item) {
456
- if (this.publishedStatuses.length === 0) return true;
457
- return this.publishedStatuses.includes(item.status);
458
- }
459
- /** コンテンツを Markdown → HTML にレンダリングし、CachedItem として返す。 */
460
- async render(item) {
461
- return this.buildCachedItem(item);
462
- }
463
- /** QueryBuilder を返す。ステータス・タグ・ページネーションなどを連鎖で指定できる。 */
464
- query() {
465
- return new QueryBuilder(this.source, this.publishedStatuses);
466
- }
467
- /** 全コンテンツを事前レンダリングしてキャッシュに保存する。 */
468
- async prefetchAll(opts) {
469
- const items = await this.list();
470
- const concurrency = opts?.concurrency ?? 3;
471
- let ok = 0;
472
- let failed = 0;
473
- for (let i = 0; i < items.length; i += concurrency) {
474
- const chunk = items.slice(i, i + concurrency);
475
- await Promise.all(
476
- chunk.map(async (item) => {
477
- try {
478
- const rendered = await this.buildCachedItem(item);
479
- await this.docCache.setItem(item.slug, rendered);
480
- ok++;
481
- } catch {
482
- failed++;
483
- }
484
- })
485
- );
486
- opts?.onProgress?.(Math.min(i + concurrency, items.length), items.length);
487
- }
488
- await this.docCache.setList({ items, cachedAt: Date.now() });
489
- return { ok, failed };
490
- }
491
- /** 静的生成用のスラッグ一覧を返す。 */
492
- async getStaticSlugs() {
493
- const items = await this.list();
494
- return items.map((item) => item.slug);
495
- }
496
- /** 指定スコープのキャッシュを無効化する。 */
497
- async revalidate(scope) {
498
- if (!this.docCache.invalidate) return;
499
- await this.docCache.invalidate(scope ?? "all");
500
- }
501
- /** Webhook ペイロードを元にキャッシュを同期する。 */
502
- async syncFromWebhook(payload) {
503
- const updated = [];
504
- if (payload?.slug) {
505
- const item = await this.find(payload.slug);
506
- if (item) {
507
- const rendered = await this.buildCachedItem(item);
508
- await this.docCache.setItem(item.slug, rendered);
509
- updated.push(item.slug);
510
- }
511
- } else {
512
- const result = await this.prefetchAll();
513
- if (result.ok > 0) {
514
- const items = await this.list();
515
- for (const item of items) updated.push(item.slug);
516
- }
517
- }
518
- return { updated };
519
- }
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
- async checkListUpdate(version) {
559
- const items = await this.list();
560
- const serverVersion = buildListVersion(items);
561
- if (serverVersion === version) return { changed: false };
562
- await this.docCache.setList({ items, cachedAt: Date.now() });
563
- return { changed: true, items };
564
- }
565
- async checkItemUpdate(slug, lastEdited) {
566
- const item = await this.find(slug);
567
- if (!item) return { changed: false };
568
- if (!this.isPublished(item)) return { changed: false };
569
- if (item.updatedAt === lastEdited) return { changed: false };
570
- const entry = await this.buildCachedItem(item);
571
- await this.docCache.setItem(slug, entry);
572
- return {
573
- changed: true,
574
- html: entry.html,
575
- item: entry.item,
576
- notionUpdatedAt: entry.notionUpdatedAt
577
- };
578
- }
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
- // ── プライベートヘルパー ────────────────────────────────────────────────
633
- async buildCachedItem(item) {
634
- const start = Date.now();
635
- this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u958B\u59CB", {
636
- slug: item.slug,
637
- pageId: item.id
638
- });
639
- let markdown;
640
- try {
641
- markdown = await this.source.loadMarkdown(item);
642
- } catch (err) {
643
- if (isCMSError(err)) throw err;
644
- throw new CMSError({
645
- code: "NOTION_MARKDOWN_FETCH_FAILED",
646
- message: "Failed to load markdown from source.",
647
- cause: err,
648
- context: {
649
- operation: "buildCachedItem:loadMarkdown",
650
- pageId: item.id,
651
- slug: item.slug
652
- }
653
- });
654
- }
655
- const cacheImage = this.hasImageCache ? buildCacheImageFn(this.imgCache, this.imageProxyBase) : void 0;
656
- let html;
657
- try {
658
- html = await renderMarkdown(markdown, {
659
- imageProxyBase: this.imageProxyBase,
660
- cacheImage,
661
- remarkPlugins: this.contentConfig?.remarkPlugins,
662
- rehypePlugins: this.contentConfig?.rehypePlugins,
663
- render: this.contentConfig?.render
664
- });
665
- } catch (err) {
666
- if (isCMSError(err)) throw err;
667
- throw new CMSError({
668
- code: "RENDERER_FAILED",
669
- message: "Failed to render markdown.",
670
- cause: err,
671
- context: {
672
- operation: "buildCachedItem:renderMarkdown",
673
- pageId: item.id,
674
- slug: item.slug
675
- }
676
- });
677
- }
678
- if (this.hooks.afterRender) {
679
- html = await this.hooks.afterRender(html, item);
680
- }
681
- let result = {
682
- html,
683
- item,
684
- notionUpdatedAt: item.updatedAt,
685
- cachedAt: Date.now()
686
- };
687
- if (this.hooks.beforeCache) {
688
- result = await this.hooks.beforeCache(result);
689
- }
690
- this.logger?.info?.("\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u5B8C\u4E86", {
691
- slug: item.slug,
692
- durationMs: Date.now() - start
693
- });
694
- return result;
695
- }
696
- };
697
- function buildListVersion(items) {
698
- return items.map((item) => `${item.id}:${item.updatedAt}`).join("|");
699
- }
700
- function createCMS(opts) {
701
- return new CMS(opts);
702
- }
703
-
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
- // src/types/plugin.ts
743
- function definePlugin(plugin) {
744
- return plugin;
745
- }
746
- export {
747
- CMS,
748
- CMSError,
749
- DEFAULT_RETRY_CONFIG,
750
- QueryBuilder,
751
- createCMS,
752
- definePlugin,
753
- getPlainText,
754
- isCMSError,
755
- isStale,
756
- mapItem,
757
- memoryCache,
758
- memoryDocumentCache,
759
- memoryImageCache,
760
- mergeHooks,
761
- mergeLoggers,
762
- noopDocumentCache,
763
- noopImageCache,
764
- sha256Hex,
765
- withRetry
766
- };