@ampless/runtime 0.2.0-alpha.0

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 ADDED
@@ -0,0 +1,530 @@
1
+ // src/storage.ts
2
+ import { formatPublicAssetUrl } from "ampless";
3
+ function createStorage(outputs) {
4
+ const storage = outputs.storage ?? null;
5
+ return {
6
+ publicAssetUrl(key) {
7
+ if (!storage) {
8
+ throw new Error(
9
+ "amplify storage output missing \u2014 run `npx ampx sandbox` (or deploy) before invoking this code path"
10
+ );
11
+ }
12
+ return formatPublicAssetUrl(storage.bucket_name, storage.aws_region, key);
13
+ },
14
+ isStorageConfigured() {
15
+ return storage !== null;
16
+ }
17
+ };
18
+ }
19
+
20
+ // src/posts.ts
21
+ import { cookies } from "next/headers";
22
+ import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
23
+ function decodeBody(value) {
24
+ if (typeof value !== "string") return value;
25
+ try {
26
+ return JSON.parse(value);
27
+ } catch {
28
+ return value;
29
+ }
30
+ }
31
+ function toCorePost(p) {
32
+ return {
33
+ postId: p.postId,
34
+ siteId: p.siteId,
35
+ slug: p.slug,
36
+ title: p.title,
37
+ excerpt: p.excerpt ?? void 0,
38
+ format: p.format ?? "markdown",
39
+ body: decodeBody(p.body),
40
+ status: p.status ?? "draft",
41
+ publishedAt: p.publishedAt ?? void 0,
42
+ tags: (p.tags ?? []).filter((t) => typeof t === "string")
43
+ };
44
+ }
45
+ function createPostsApi(outputs) {
46
+ const client = generateServerClientUsingCookies({
47
+ // generateServerClientUsingCookies expects the resourcesConfig shape
48
+ // — the full amplify_outputs.json satisfies it at runtime.
49
+ config: outputs,
50
+ cookies,
51
+ authMode: "apiKey"
52
+ });
53
+ return {
54
+ async listPublishedPosts(opts = {}) {
55
+ const { data, errors } = await client.queries.listPublishedPosts({
56
+ siteId: opts.siteId ?? "default",
57
+ from: opts.from,
58
+ to: opts.to,
59
+ limit: opts.limit ?? 20,
60
+ nextToken: opts.nextToken
61
+ });
62
+ if (errors) throw new Error(errors[0]?.message ?? "Failed to list posts");
63
+ const items = (data?.items ?? []).filter((p) => p !== null).map(toCorePost);
64
+ return { items, nextToken: data?.nextToken ?? null };
65
+ },
66
+ async getPublishedPost(slug, opts = {}) {
67
+ const { data, errors } = await client.queries.getPublishedPost({
68
+ siteId: opts.siteId ?? "default",
69
+ slug
70
+ });
71
+ if (errors) throw new Error(errors[0]?.message ?? "Failed to get post");
72
+ return data ? toCorePost(data) : null;
73
+ },
74
+ async listPostsByTag(tag, opts = {}) {
75
+ const { data, errors } = await client.queries.listPostsByTag({
76
+ siteId: opts.siteId ?? "default",
77
+ tag,
78
+ limit: opts.limit ?? 20,
79
+ nextToken: opts.nextToken
80
+ });
81
+ if (errors) throw new Error(errors[0]?.message ?? "Failed to list posts by tag");
82
+ const items = (data?.items ?? []).filter((p) => p !== null).map(toCorePost);
83
+ return { items, nextToken: data?.nextToken ?? null };
84
+ }
85
+ };
86
+ }
87
+
88
+ // src/site-settings.ts
89
+ import { DEFAULT_SITE_ID, siteFor, unflattenSettings } from "ampless";
90
+ function createSiteSettings(cmsConfig, storage) {
91
+ async function fetchRemote(siteId) {
92
+ if (!storage.isStorageConfigured()) return null;
93
+ let url;
94
+ try {
95
+ url = storage.publicAssetUrl(`public/site-settings/${siteId}.json`);
96
+ } catch {
97
+ return null;
98
+ }
99
+ const res = await fetch(url, { next: { revalidate: 60, tags: [`site-settings:${siteId}`] } });
100
+ if (!res.ok) return null;
101
+ const flat = await res.json();
102
+ return unflattenSettings(flat);
103
+ }
104
+ return {
105
+ async loadSiteSettings(siteId = DEFAULT_SITE_ID) {
106
+ const remote = await fetchRemote(siteId).catch(() => null);
107
+ const baseSite = siteFor(siteId, cmsConfig);
108
+ return {
109
+ site: {
110
+ name: remote?.site?.name ?? baseSite.name,
111
+ url: remote?.site?.url ?? baseSite.url,
112
+ description: remote?.site?.description ?? baseSite.description
113
+ },
114
+ media: {
115
+ imageDisplay: remote?.media?.imageDisplay ?? cmsConfig.media?.imageDisplay,
116
+ imageMaxWidth: remote?.media?.imageMaxWidth ?? cmsConfig.media?.imageMaxWidth,
117
+ processing: {
118
+ maxDimension: remote?.media?.processing?.maxDimension ?? cmsConfig.media?.processing?.maxDimension,
119
+ format: remote?.media?.processing?.format ?? cmsConfig.media?.processing?.format,
120
+ quality: remote?.media?.processing?.quality ?? cmsConfig.media?.processing?.quality,
121
+ losslessForPng: remote?.media?.processing?.losslessForPng ?? cmsConfig.media?.processing?.losslessForPng
122
+ }
123
+ },
124
+ dateFormat: remote?.dateFormat ?? cmsConfig.dateFormat,
125
+ timezone: remote?.timezone ?? cmsConfig.timezone
126
+ };
127
+ }
128
+ };
129
+ }
130
+
131
+ // src/seo.ts
132
+ import { DEFAULT_SITE_ID as DEFAULT_SITE_ID2 } from "ampless";
133
+ function isPlugin(p) {
134
+ return typeof p === "object" && p !== null && "apiVersion" in p;
135
+ }
136
+ function createSeo(cmsConfig, settingsApi) {
137
+ const plugins = (cmsConfig.plugins ?? []).filter(isPlugin);
138
+ return {
139
+ async postMetadata(post, siteId = DEFAULT_SITE_ID2) {
140
+ const settings = await settingsApi.loadSiteSettings(siteId);
141
+ const accum = {};
142
+ for (const plugin of plugins) {
143
+ if (!plugin.metadata) continue;
144
+ const m = plugin.metadata(post, settings.site);
145
+ Object.assign(accum, m, {
146
+ openGraph: { ...accum.openGraph ?? {}, ...m.openGraph ?? {} },
147
+ twitter: { ...accum.twitter ?? {}, ...m.twitter ?? {} },
148
+ alternates: {
149
+ ...accum.alternates ?? {},
150
+ ...m.alternates ?? {},
151
+ types: { ...accum.alternates?.types ?? {}, ...m.alternates?.types ?? {} }
152
+ }
153
+ });
154
+ }
155
+ return accum;
156
+ },
157
+ async siteMetadata(siteId = DEFAULT_SITE_ID2) {
158
+ const settings = await settingsApi.loadSiteSettings(siteId);
159
+ const accum = {
160
+ title: settings.site.name,
161
+ description: settings.site.description
162
+ };
163
+ for (const plugin of plugins) {
164
+ if (!plugin.siteMetadata) continue;
165
+ const m = plugin.siteMetadata(settings.site);
166
+ Object.assign(accum, m, {
167
+ openGraph: { ...accum.openGraph ?? {}, ...m.openGraph ?? {} },
168
+ twitter: { ...accum.twitter ?? {}, ...m.twitter ?? {} },
169
+ alternates: {
170
+ ...accum.alternates ?? {},
171
+ ...m.alternates ?? {},
172
+ types: { ...accum.alternates?.types ?? {}, ...m.alternates?.types ?? {} }
173
+ }
174
+ });
175
+ }
176
+ return accum;
177
+ }
178
+ };
179
+ }
180
+
181
+ // src/theme-active.ts
182
+ import { headers } from "next/headers";
183
+ import { DEFAULT_SITE_ID as DEFAULT_SITE_ID3 } from "ampless";
184
+ function createThemeActive(registry, storage) {
185
+ async function fetchActiveFromCache(siteId) {
186
+ if (!storage.isStorageConfigured()) return null;
187
+ let url;
188
+ try {
189
+ url = storage.publicAssetUrl(`public/site-settings/${siteId}.json`);
190
+ } catch {
191
+ return null;
192
+ }
193
+ const res = await fetch(url, { next: { revalidate: 60, tags: [`site-settings:${siteId}`] } });
194
+ if (!res.ok) return null;
195
+ const flat = await res.json();
196
+ const v = flat["theme.active"];
197
+ return typeof v === "string" ? v : null;
198
+ }
199
+ return {
200
+ async resolveActiveTheme(siteId = DEFAULT_SITE_ID3) {
201
+ let previewOverride = null;
202
+ try {
203
+ const h = await headers();
204
+ previewOverride = h.get("x-preview-theme");
205
+ } catch {
206
+ }
207
+ if (previewOverride && previewOverride in registry.themes) {
208
+ const mod2 = registry.themes[previewOverride];
209
+ if (mod2) return { name: previewOverride, module: mod2 };
210
+ }
211
+ const stored = await fetchActiveFromCache(siteId).catch(() => null);
212
+ const name = stored && stored in registry.themes ? stored : registry.defaultTheme;
213
+ const mod = registry.themes[name] ?? registry.themes[registry.defaultTheme];
214
+ if (!mod) {
215
+ throw new Error(
216
+ `themes registry is empty \u2014 at least one theme must be registered before resolveActiveTheme is called`
217
+ );
218
+ }
219
+ return { name, module: mod };
220
+ }
221
+ };
222
+ }
223
+
224
+ // src/theme-config.ts
225
+ import {
226
+ DEFAULT_SITE_ID as DEFAULT_SITE_ID4,
227
+ resolveThemeValues,
228
+ themeSettingKey
229
+ } from "ampless";
230
+ function createThemeConfig(themeActive, storage) {
231
+ async function fetchRemote(siteId) {
232
+ if (!storage.isStorageConfigured()) return null;
233
+ let url;
234
+ try {
235
+ url = storage.publicAssetUrl(`public/site-settings/${siteId}.json`);
236
+ } catch {
237
+ return null;
238
+ }
239
+ const res = await fetch(url, { next: { revalidate: 60, tags: [`site-settings:${siteId}`] } });
240
+ if (!res.ok) return null;
241
+ return await res.json();
242
+ }
243
+ return {
244
+ async loadThemeConfig(siteId = DEFAULT_SITE_ID4) {
245
+ const [active, flat] = await Promise.all([
246
+ themeActive.resolveActiveTheme(siteId),
247
+ fetchRemote(siteId).catch(() => null)
248
+ ]);
249
+ const manifest = active.module.manifest;
250
+ const stored = {};
251
+ if (flat) {
252
+ for (const field of manifest.fields) {
253
+ const k = themeSettingKey(field.key);
254
+ if (k in flat) stored[k] = flat[k];
255
+ }
256
+ }
257
+ const values = resolveThemeValues(manifest, stored);
258
+ const cssVars = collectCssVars(manifest.fields, values);
259
+ return { activeTheme: active.name, manifest, values, cssVars };
260
+ }
261
+ };
262
+ }
263
+ function collectCssVars(fields, values) {
264
+ const out = {};
265
+ for (const field of fields) {
266
+ if (field.type === "linkList") continue;
267
+ if (!field.cssVar) continue;
268
+ const v = values[field.key];
269
+ if (v) out[field.cssVar] = v;
270
+ }
271
+ return out;
272
+ }
273
+ function renderThemeCss(cssVars) {
274
+ const lines = Object.entries(cssVars).map(([name, value]) => ` ${name}: ${value};`);
275
+ if (lines.length === 0) return "";
276
+ return `:root {
277
+ ${lines.join("\n")}
278
+ }`;
279
+ }
280
+
281
+ // src/rendering.ts
282
+ function escape(s) {
283
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
284
+ }
285
+ function renderTiptap(node) {
286
+ if (node.type === "text") {
287
+ let html = escape(node.text ?? "");
288
+ for (const mark of node.marks ?? []) {
289
+ if (mark.type === "bold") html = `<strong>${html}</strong>`;
290
+ else if (mark.type === "italic") html = `<em>${html}</em>`;
291
+ else if (mark.type === "code") html = `<code>${html}</code>`;
292
+ else if (mark.type === "strike") html = `<s>${html}</s>`;
293
+ else if (mark.type === "link") {
294
+ const href = escape(String(mark.attrs?.href ?? "#"));
295
+ html = `<a href="${href}" target="_blank" rel="noopener">${html}</a>`;
296
+ }
297
+ }
298
+ return html;
299
+ }
300
+ const children = (node.content ?? []).map(renderTiptap).join("");
301
+ switch (node.type) {
302
+ case "doc":
303
+ return children;
304
+ case "paragraph":
305
+ return `<p>${children}</p>`;
306
+ case "heading": {
307
+ const level = Number(node.attrs?.level ?? 1);
308
+ return `<h${level}>${children}</h${level}>`;
309
+ }
310
+ case "bulletList":
311
+ return `<ul>${children}</ul>`;
312
+ case "orderedList":
313
+ return `<ol>${children}</ol>`;
314
+ case "listItem":
315
+ return `<li>${children}</li>`;
316
+ case "codeBlock": {
317
+ const lang = node.attrs?.language ? ` class="language-${escape(String(node.attrs.language))}"` : "";
318
+ return `<pre><code${lang}>${children}</code></pre>`;
319
+ }
320
+ case "blockquote":
321
+ return `<blockquote>${children}</blockquote>`;
322
+ case "hardBreak":
323
+ return "<br />";
324
+ case "horizontalRule":
325
+ return "<hr />";
326
+ case "image": {
327
+ const src = escape(String(node.attrs?.src ?? ""));
328
+ const alt = escape(String(node.attrs?.alt ?? ""));
329
+ const title = node.attrs?.title ? ` title="${escape(String(node.attrs.title))}"` : "";
330
+ const display = node.attrs?.display ? ` data-display="${escape(String(node.attrs.display))}"` : "";
331
+ return `<img src="${src}" alt="${alt}"${title}${display} loading="lazy" />`;
332
+ }
333
+ default:
334
+ return children;
335
+ }
336
+ }
337
+ function renderMarkdown(md) {
338
+ const lines = md.split("\n");
339
+ const out = [];
340
+ let i = 0;
341
+ while (i < lines.length) {
342
+ const line = lines[i] ?? "";
343
+ if (line.startsWith("# ")) {
344
+ out.push(`<h1>${escape(line.slice(2))}</h1>`);
345
+ i++;
346
+ } else if (line.startsWith("## ")) {
347
+ out.push(`<h2>${escape(line.slice(3))}</h2>`);
348
+ i++;
349
+ } else if (line.startsWith("```")) {
350
+ const lang = line.slice(3).trim();
351
+ const code = [];
352
+ i++;
353
+ while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
354
+ code.push(lines[i] ?? "");
355
+ i++;
356
+ }
357
+ i++;
358
+ out.push(
359
+ `<pre><code${lang ? ` class="language-${escape(lang)}"` : ""}>${escape(code.join("\n"))}</code></pre>`
360
+ );
361
+ } else if (line.startsWith("- ")) {
362
+ const items = [];
363
+ while (i < lines.length && (lines[i] ?? "").startsWith("- ")) {
364
+ items.push(`<li>${renderInlineMarkdown((lines[i] ?? "").slice(2))}</li>`);
365
+ i++;
366
+ }
367
+ out.push(`<ul>${items.join("")}</ul>`);
368
+ } else if (line.trim() === "") {
369
+ i++;
370
+ } else {
371
+ out.push(`<p>${renderInlineMarkdown(line)}</p>`);
372
+ i++;
373
+ }
374
+ }
375
+ return out.join("\n");
376
+ }
377
+ function renderInlineMarkdown(text) {
378
+ return text.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
379
+ }
380
+ function renderBody(post) {
381
+ if (post.format === "html") return String(post.body);
382
+ if (post.format === "markdown") return renderMarkdown(String(post.body));
383
+ if (post.format === "tiptap") {
384
+ if (typeof post.body === "string") return post.body;
385
+ return renderTiptap(post.body);
386
+ }
387
+ return "";
388
+ }
389
+ function tiptapToHtml(doc) {
390
+ if (typeof doc === "string") return doc;
391
+ return renderTiptap(doc);
392
+ }
393
+ function markdownToHtml(md) {
394
+ return renderMarkdown(md);
395
+ }
396
+ function tiptapToMarkdown(doc) {
397
+ if (typeof doc === "string") return htmlToMarkdown(doc);
398
+ const node = doc;
399
+ return tiptapNodeToMarkdown(node).trim() + "\n";
400
+ }
401
+ function tiptapNodeToMarkdown(node) {
402
+ if (node.type === "text") {
403
+ let txt = node.text ?? "";
404
+ for (const mark of node.marks ?? []) {
405
+ if (mark.type === "bold") txt = `**${txt}**`;
406
+ else if (mark.type === "italic") txt = `*${txt}*`;
407
+ else if (mark.type === "code") txt = `\`${txt}\``;
408
+ else if (mark.type === "strike") txt = `~~${txt}~~`;
409
+ else if (mark.type === "link") txt = `[${txt}](${String(mark.attrs?.href ?? "#")})`;
410
+ }
411
+ return txt;
412
+ }
413
+ const children = (node.content ?? []).map(tiptapNodeToMarkdown).join("");
414
+ switch (node.type) {
415
+ case "doc":
416
+ return children;
417
+ case "paragraph":
418
+ return children + "\n\n";
419
+ case "heading": {
420
+ const level = Math.max(1, Math.min(6, Number(node.attrs?.level ?? 1)));
421
+ return "#".repeat(level) + " " + children + "\n\n";
422
+ }
423
+ case "bulletList":
424
+ return children + "\n";
425
+ case "orderedList":
426
+ return children + "\n";
427
+ case "listItem": {
428
+ const trimmed = children.replace(/\n+$/, "");
429
+ return "- " + trimmed + "\n";
430
+ }
431
+ case "codeBlock": {
432
+ const lang = node.attrs?.language ? String(node.attrs.language) : "";
433
+ return "```" + lang + "\n" + children + "\n```\n\n";
434
+ }
435
+ case "blockquote":
436
+ return children.replace(/\n+$/, "").split("\n").map((l) => "> " + l).join("\n") + "\n\n";
437
+ case "hardBreak":
438
+ return " \n";
439
+ case "horizontalRule":
440
+ return "\n---\n\n";
441
+ case "image": {
442
+ const src = String(node.attrs?.src ?? "");
443
+ const alt = String(node.attrs?.alt ?? "");
444
+ return `![${alt}](${src})`;
445
+ }
446
+ default:
447
+ return children;
448
+ }
449
+ }
450
+ function htmlToMarkdown(html) {
451
+ let md = html;
452
+ md = md.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, text) => {
453
+ return "\n" + "#".repeat(Number(level)) + " " + String(text).trim() + "\n\n";
454
+ });
455
+ md = md.replace(
456
+ /<pre[^>]*><code[^>]*(?:\sclass="language-([^"]+)")?[^>]*>([\s\S]*?)<\/code><\/pre>/gi,
457
+ (_, lang, code) => {
458
+ return "\n```" + (lang ?? "") + "\n" + String(code) + "\n```\n\n";
459
+ }
460
+ );
461
+ md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
462
+ return "\n" + String(content).trim().split("\n").map((l) => "> " + l).join("\n") + "\n\n";
463
+ });
464
+ md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
465
+ return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n") + "\n";
466
+ });
467
+ md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_, items) => {
468
+ let i = 1;
469
+ return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, () => `${i++}. $1
470
+ `) + "\n";
471
+ });
472
+ md = md.replace(/<hr\s*\/?>/gi, "\n---\n\n");
473
+ md = md.replace(/<br\s*\/?>/gi, " \n");
474
+ md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, "$1\n\n");
475
+ md = md.replace(/<img[^>]*?src="([^"]*)"[^>]*?alt="([^"]*)"[^>]*?\/?>/gi, "![$2]($1)");
476
+ md = md.replace(/<img[^>]*?alt="([^"]*)"[^>]*?src="([^"]*)"[^>]*?\/?>/gi, "![$1]($2)");
477
+ md = md.replace(/<img[^>]*?src="([^"]*)"[^>]*?\/?>/gi, "![]($1)");
478
+ md = md.replace(/<a[^>]*?href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
479
+ md = md.replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi, "**$2**");
480
+ md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, "*$2*");
481
+ md = md.replace(/<s>([\s\S]*?)<\/s>/gi, "~~$1~~");
482
+ md = md.replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`");
483
+ md = md.replace(/<\/?[^>]+>/g, "");
484
+ md = md.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ");
485
+ md = md.replace(/\n{3,}/g, "\n\n");
486
+ return md.trim() + "\n";
487
+ }
488
+
489
+ // src/index.ts
490
+ function createAmpless(opts) {
491
+ const { outputs, cmsConfig, themes } = opts;
492
+ const storage = createStorage(outputs);
493
+ const posts = createPostsApi(outputs);
494
+ const settings = createSiteSettings(cmsConfig, storage);
495
+ const seo = createSeo(cmsConfig, settings);
496
+ const themeActive = createThemeActive(themes, storage);
497
+ const themeConfig = createThemeConfig(themeActive, storage);
498
+ return {
499
+ listPublishedPosts: (o) => posts.listPublishedPosts(o),
500
+ getPublishedPost: (slug, o) => posts.getPublishedPost(slug, o),
501
+ listPostsByTag: (tag, o) => posts.listPostsByTag(tag, o),
502
+ loadSiteSettings: (siteId) => settings.loadSiteSettings(siteId),
503
+ resolveActiveTheme: (siteId) => themeActive.resolveActiveTheme(siteId),
504
+ loadThemeConfig: (siteId) => themeConfig.loadThemeConfig(siteId),
505
+ postMetadata: (post, siteId) => seo.postMetadata(post, siteId),
506
+ siteMetadata: (siteId) => seo.siteMetadata(siteId),
507
+ renderBody: (post) => renderBody(post),
508
+ renderThemeCss: (cssVars) => renderThemeCss(cssVars),
509
+ publicAssetUrl: (key) => storage.publicAssetUrl(key),
510
+ isStorageConfigured: () => storage.isStorageConfigured(),
511
+ outputs,
512
+ cmsConfig,
513
+ themes,
514
+ posts,
515
+ settings,
516
+ seo,
517
+ themeActive,
518
+ themeConfig,
519
+ storageApi: storage
520
+ };
521
+ }
522
+ export {
523
+ createAmpless,
524
+ htmlToMarkdown,
525
+ markdownToHtml,
526
+ renderBody,
527
+ renderThemeCss,
528
+ tiptapToHtml,
529
+ tiptapToMarkdown
530
+ };
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { Config } from 'ampless';
3
+
4
+ interface CreateMiddlewareOpts {
5
+ cmsConfig: Config;
6
+ }
7
+ type MiddlewareFn = (request: NextRequest) => NextResponse;
8
+ /**
9
+ * Build the ampless public-site middleware. Performs:
10
+ *
11
+ * - hostname → siteId resolution (multi-site rewrites)
12
+ * - `/path` → `/site/<siteId>/path` internal rewrite
13
+ * - `<slug>.html` → `/site/<siteId>/raw/<slug>.html` (bare HTML route)
14
+ * - `?previewTheme=<name>` → `x-preview-theme` header forwarding
15
+ * - `Cache-Control: private, no-store` in multi-site mode (Amplify
16
+ * Hosting's CloudFront cache key doesn't include Host, so SSR
17
+ * responses would cross-contaminate at the same path)
18
+ *
19
+ * The factory captures the multi-site flag at construction time so the
20
+ * hot path stays a pair of cheap header lookups.
21
+ */
22
+ declare function createAmplessMiddleware({ cmsConfig }: CreateMiddlewareOpts): MiddlewareFn;
23
+ /**
24
+ * Default Next.js middleware matcher config. Exposed as a constant so
25
+ * templates can re-export it directly:
26
+ *
27
+ * export const config = defaultMatcherConfig
28
+ *
29
+ * Excludes admin / api / login / static assets / amplify_outputs.json
30
+ * — without these exclusions middleware would rewrite legitimate
31
+ * non-blog routes into the public site tree.
32
+ */
33
+ declare const defaultMatcherConfig: {
34
+ matcher: string[];
35
+ };
36
+
37
+ export { type CreateMiddlewareOpts, type MiddlewareFn, createAmplessMiddleware, defaultMatcherConfig };
@@ -0,0 +1,43 @@
1
+ // src/middleware.ts
2
+ import { NextResponse } from "next/server";
3
+ import { resolveSiteId, isMultiSite } from "ampless";
4
+ var RAW_HTML_PATH_RE = /^\/([^/]+\.html)$/;
5
+ function createAmplessMiddleware({ cmsConfig }) {
6
+ const MULTI_SITE = isMultiSite(cmsConfig);
7
+ return function middleware(request) {
8
+ const host = (request.headers.get("host") ?? "").split(":")[0];
9
+ const siteId = resolveSiteId(host ?? "", cmsConfig);
10
+ if (!siteId) {
11
+ return new NextResponse("Site not found", { status: 404 });
12
+ }
13
+ const url = request.nextUrl.clone();
14
+ if (!url.pathname.startsWith("/site/")) {
15
+ const rawMatch = RAW_HTML_PATH_RE.exec(url.pathname);
16
+ if (rawMatch) {
17
+ url.pathname = `/site/${siteId}/raw/${rawMatch[1]}`;
18
+ } else {
19
+ const tail = url.pathname === "/" ? "" : url.pathname;
20
+ url.pathname = `/site/${siteId}${tail}`;
21
+ }
22
+ }
23
+ const previewTheme = url.searchParams.get("previewTheme");
24
+ const requestHeaders = new Headers(request.headers);
25
+ requestHeaders.set("x-site-id", siteId);
26
+ if (previewTheme) requestHeaders.set("x-preview-theme", previewTheme);
27
+ const response = NextResponse.rewrite(url, { request: { headers: requestHeaders } });
28
+ response.headers.set("x-site-id", siteId);
29
+ if (MULTI_SITE) {
30
+ response.headers.set("Cache-Control", "private, no-store");
31
+ }
32
+ return response;
33
+ };
34
+ }
35
+ var defaultMatcherConfig = {
36
+ matcher: [
37
+ "/((?!admin|api|login|_next/static|_next/image|favicon\\.ico|amplify_outputs\\.json).*)"
38
+ ]
39
+ };
40
+ export {
41
+ createAmplessMiddleware,
42
+ defaultMatcherConfig
43
+ };
@@ -0,0 +1,77 @@
1
+ import { Ampless } from '../index.js';
2
+ import 'ampless';
3
+ import 'next';
4
+
5
+ interface Ctx$3 {
6
+ params: Promise<{
7
+ siteId: string;
8
+ slug: string;
9
+ }>;
10
+ }
11
+ type OgRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
12
+ declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
13
+
14
+ interface Ctx$2 {
15
+ params: Promise<{
16
+ siteId: string;
17
+ }>;
18
+ }
19
+ type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
20
+ /**
21
+ * Sitemap route delegate. Looks up the active theme for the request's
22
+ * siteId and forwards to whichever `routes.sitemap` handler the theme
23
+ * provides. Themes without a sitemap handler return 404.
24
+ */
25
+ declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
26
+
27
+ interface Ctx$1 {
28
+ params: Promise<{
29
+ siteId: string;
30
+ }>;
31
+ }
32
+ type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
33
+ /**
34
+ * Feed route delegate. Looks up the active theme for the request's
35
+ * siteId and forwards to whichever `routes.feed` handler the theme
36
+ * provides. Themes without a feed handler return 404.
37
+ */
38
+ declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
39
+
40
+ interface Ctx {
41
+ params: Promise<{
42
+ siteId: string;
43
+ slug: string;
44
+ }>;
45
+ }
46
+ type RawRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
47
+ /**
48
+ * Bare HTML route. Returns the published post's rendered body as the
49
+ * entire HTTP response — no Next.js root layout, no theme chrome.
50
+ *
51
+ * Reached via the slug-suffix convention: middleware rewrites
52
+ * `/<slug>.html` → `/site/<siteId>/raw/<slug>.html`. The post is
53
+ * looked up by the full slug (including the `.html` part), so what
54
+ * the admin types in the slug field IS the URL.
55
+ *
56
+ * Why this is a route handler (not a page): Next.js's root layout
57
+ * always emits `<html>` / `<head>` / `<body>`, which means a normal
58
+ * page can't replace the document. A `route.ts` returns a Response
59
+ * directly and bypasses React rendering entirely.
60
+ *
61
+ * Format pairing:
62
+ * - `format: 'html'` with a full `<!DOCTYPE html>...</html>` body
63
+ * → the body lands in the response unchanged. Best fit for
64
+ * custom landing pages with their own `<head>`, styles, scripts.
65
+ * - `format: 'tiptap'` / `'markdown'` → renderBody returns an
66
+ * HTML fragment. Browsers render fragments fine, but the page
67
+ * ships without `<!DOCTYPE>` / `<head>` / `<title>`. If you care
68
+ * about those, use `format: 'html'`.
69
+ *
70
+ * Trust model: the response body is the editor's content verbatim,
71
+ * with no sanitization. Same trust assumption as `format: 'html'`
72
+ * post bodies on the regular path — see
73
+ * docs/architecture/04-access-layer-mcp.md §"editor の信頼モデル".
74
+ */
75
+ declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
76
+
77
+ export { type FeedRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, createFeedRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler };