@ampless/runtime 0.2.0-alpha.9 → 1.0.0-alpha.13
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/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/dispatchers/index.d.ts +21 -11
- package/dist/dispatchers/index.js +10 -20
- package/dist/index.d.ts +29 -26
- package/dist/index.js +174 -95
- package/dist/middleware.d.ts +16 -14
- package/dist/middleware.js +8 -17
- package/dist/routes/index.d.ts +48 -74
- package/dist/routes/index.js +84 -79
- package/package.json +4 -3
package/README.ja.md
CHANGED
|
@@ -74,7 +74,7 @@ export const config = defaultMatcherConfig
|
|
|
74
74
|
|
|
75
75
|
- `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
|
|
76
76
|
- `@ampless/runtime/middleware` — `createAmplessMiddleware`、`defaultMatcherConfig`
|
|
77
|
-
- `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`
|
|
77
|
+
- `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createUnderscoreRouteHandler`
|
|
78
78
|
- `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`、`createThemePostDispatcher`、`createThemeTagDispatcher`(それぞれ対応する `*Metadata` ファクトリーあり)
|
|
79
79
|
|
|
80
80
|
## テンプレートに残るもの
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ export const config = defaultMatcherConfig
|
|
|
74
74
|
|
|
75
75
|
- `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
|
|
76
76
|
- `@ampless/runtime/middleware` — `createAmplessMiddleware`, `defaultMatcherConfig`
|
|
77
|
-
- `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `
|
|
77
|
+
- `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createUnderscoreRouteHandler`
|
|
78
78
|
- `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`, `createThemePostDispatcher`, `createThemeTagDispatcher` (each with a matching `*Metadata` factory)
|
|
79
79
|
|
|
80
80
|
## What's still in the template
|
|
@@ -11,9 +11,9 @@ interface Props$2 {
|
|
|
11
11
|
type ThemeHomeDispatcher = (props: Props$2) => Promise<ReactNode>;
|
|
12
12
|
type ThemeHomeMetadata = (props: Props$2) => Promise<Metadata>;
|
|
13
13
|
/**
|
|
14
|
-
* Home page dispatcher. Resolves the active theme
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Home page dispatcher. Resolves the active theme and renders the
|
|
15
|
+
* theme's `components.Home` server component with the same `params`
|
|
16
|
+
* Promise it was passed.
|
|
17
17
|
*/
|
|
18
18
|
declare function createThemeHomeDispatcher(ampless: Ampless): ThemeHomeDispatcher;
|
|
19
19
|
/** generateMetadata factory for the home dispatcher. */
|
|
@@ -31,14 +31,24 @@ type ThemePostMetadata = (props: Props$1) => Promise<Metadata>;
|
|
|
31
31
|
* Post page dispatcher. Resolves the active theme and renders the
|
|
32
32
|
* theme's `components.Post` server component.
|
|
33
33
|
*
|
|
34
|
-
* Before delegating, the dispatcher peeks at the post's
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the
|
|
34
|
+
* Before delegating, the dispatcher peeks at the post's data and
|
|
35
|
+
* 308-redirects to `/_/<slug>` for both formats that need to bypass
|
|
36
|
+
* the theme's post page:
|
|
37
|
+
*
|
|
38
|
+
* - `metadata.no_layout === true` — the body is its own complete
|
|
39
|
+
* HTML document and ships as the entire response. The unified
|
|
40
|
+
* route handler at `/_/<slug>` emits the body verbatim.
|
|
41
|
+
* - `format === 'static'` — the body is a manifest pointing at a
|
|
42
|
+
* bundle of files in S3. The unified route handler then 308s
|
|
43
|
+
* again to `/_/<slug>/` (trailing slash) so relative paths inside
|
|
44
|
+
* the bundle resolve correctly.
|
|
45
|
+
*
|
|
46
|
+
* Why the redirect lands on `/_/<slug>` rather than `/_/<slug>/` for
|
|
47
|
+
* static posts: trailing-slash anchoring is the unified route
|
|
48
|
+
* handler's responsibility — keeps the dispatcher format-agnostic
|
|
49
|
+
* and the public URL pattern symmetric (no_layout and static both
|
|
50
|
+
* settle on `/_/<slug>(/...)`). One extra 308 round-trip is cheap;
|
|
51
|
+
* it deduplicates the trailing-slash branch into a single place.
|
|
42
52
|
*
|
|
43
53
|
* The metadata peek is an extra AppSync call before theme resolve,
|
|
44
54
|
* but it's the same query the theme's Post component would make
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
// src/dispatchers/home.ts
|
|
2
2
|
function createThemeHomeDispatcher(ampless) {
|
|
3
3
|
return async function SiteHomeDispatcher({ params }) {
|
|
4
|
-
const {
|
|
5
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
4
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
6
5
|
const Home = module.components.Home;
|
|
7
6
|
return await Home({ params });
|
|
8
7
|
};
|
|
9
8
|
}
|
|
10
9
|
function createThemeHomeMetadata(ampless) {
|
|
11
10
|
return async function generateMetadata({ params }) {
|
|
12
|
-
const {
|
|
13
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
11
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
14
12
|
const fn = module.metadata?.Home;
|
|
15
13
|
return fn ? await fn({ params }) : {};
|
|
16
14
|
};
|
|
@@ -20,17 +18,12 @@ function createThemeHomeMetadata(ampless) {
|
|
|
20
18
|
import { notFound, redirect } from "next/navigation";
|
|
21
19
|
function createThemePostDispatcher(ampless) {
|
|
22
20
|
return async function SitePostDispatcher({ params }) {
|
|
23
|
-
const {
|
|
24
|
-
const post = await ampless.getPublishedPost(slug
|
|
25
|
-
if (post?.metadata?.no_layout === true) {
|
|
26
|
-
redirect(`/
|
|
21
|
+
const { slug } = await params;
|
|
22
|
+
const post = await ampless.getPublishedPost(slug);
|
|
23
|
+
if (post?.metadata?.no_layout === true || post?.format === "static") {
|
|
24
|
+
redirect(`/_/${slug}`);
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
const body = post.body ?? null;
|
|
30
|
-
const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
|
|
31
|
-
redirect(`/${slug}/${entrypoint}`);
|
|
32
|
-
}
|
|
33
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
26
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
34
27
|
const Post = module.components.Post;
|
|
35
28
|
if (!Post) notFound();
|
|
36
29
|
return await Post({ params });
|
|
@@ -38,8 +31,7 @@ function createThemePostDispatcher(ampless) {
|
|
|
38
31
|
}
|
|
39
32
|
function createThemePostMetadata(ampless) {
|
|
40
33
|
return async function generateMetadata({ params }) {
|
|
41
|
-
const {
|
|
42
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
34
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
43
35
|
const fn = module.metadata?.Post;
|
|
44
36
|
return fn ? await fn({ params }) : {};
|
|
45
37
|
};
|
|
@@ -49,8 +41,7 @@ function createThemePostMetadata(ampless) {
|
|
|
49
41
|
import { notFound as notFound2 } from "next/navigation";
|
|
50
42
|
function createThemeTagDispatcher(ampless) {
|
|
51
43
|
return async function SiteTagDispatcher({ params }) {
|
|
52
|
-
const {
|
|
53
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
44
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
54
45
|
const Tag = module.components.Tag;
|
|
55
46
|
if (!Tag) notFound2();
|
|
56
47
|
return await Tag({ params });
|
|
@@ -58,8 +49,7 @@ function createThemeTagDispatcher(ampless) {
|
|
|
58
49
|
}
|
|
59
50
|
function createThemeTagMetadata(ampless) {
|
|
60
51
|
return async function generateMetadata({ params }) {
|
|
61
|
-
const {
|
|
62
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
52
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
63
53
|
const fn = module.metadata?.Tag;
|
|
64
54
|
return fn ? await fn({ params }) : {};
|
|
65
55
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,6 @@ interface StorageApi {
|
|
|
25
25
|
|
|
26
26
|
interface PublicPostShape {
|
|
27
27
|
postId: string;
|
|
28
|
-
siteId: string;
|
|
29
28
|
slug: string;
|
|
30
29
|
title: string;
|
|
31
30
|
excerpt?: string | null;
|
|
@@ -41,7 +40,6 @@ interface PublicPostConnectionShape {
|
|
|
41
40
|
nextToken?: string | null;
|
|
42
41
|
}
|
|
43
42
|
interface ListPostsOptions {
|
|
44
|
-
siteId?: string;
|
|
45
43
|
/** ISO 8601 timestamp; SK lower bound (inclusive). */
|
|
46
44
|
from?: string;
|
|
47
45
|
/** ISO 8601 timestamp; SK upper bound (inclusive). */
|
|
@@ -51,7 +49,6 @@ interface ListPostsOptions {
|
|
|
51
49
|
nextToken?: string;
|
|
52
50
|
}
|
|
53
51
|
interface ListPostsByTagOptions {
|
|
54
|
-
siteId?: string;
|
|
55
52
|
limit?: number;
|
|
56
53
|
nextToken?: string;
|
|
57
54
|
}
|
|
@@ -61,9 +58,7 @@ interface ListPostsResult {
|
|
|
61
58
|
}
|
|
62
59
|
interface PostsApi {
|
|
63
60
|
listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
|
|
64
|
-
getPublishedPost(slug: string
|
|
65
|
-
siteId?: string;
|
|
66
|
-
}): Promise<Post | null>;
|
|
61
|
+
getPublishedPost(slug: string): Promise<Post | null>;
|
|
67
62
|
listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
|
|
68
63
|
}
|
|
69
64
|
|
|
@@ -87,18 +82,18 @@ interface EffectiveSiteSettings {
|
|
|
87
82
|
timezone?: string;
|
|
88
83
|
}
|
|
89
84
|
interface SiteSettingsApi {
|
|
90
|
-
loadSiteSettings(
|
|
85
|
+
loadSiteSettings(): Promise<EffectiveSiteSettings>;
|
|
91
86
|
}
|
|
92
87
|
|
|
93
88
|
interface SeoApi {
|
|
94
|
-
postMetadata(post: Post
|
|
95
|
-
siteMetadata(
|
|
89
|
+
postMetadata(post: Post): Promise<Metadata>;
|
|
90
|
+
siteMetadata(): Promise<Metadata>;
|
|
96
91
|
}
|
|
97
92
|
|
|
98
93
|
interface ThemesRegistry {
|
|
99
94
|
/** Map of theme name → loaded theme module. */
|
|
100
95
|
themes: Record<string, ThemeModule>;
|
|
101
|
-
/** Name used when no `theme.active` override is stored
|
|
96
|
+
/** Name used when no `theme.active` override is stored. */
|
|
102
97
|
defaultTheme: string;
|
|
103
98
|
}
|
|
104
99
|
interface ResolvedTheme {
|
|
@@ -106,7 +101,7 @@ interface ResolvedTheme {
|
|
|
106
101
|
module: ThemeModule;
|
|
107
102
|
}
|
|
108
103
|
interface ThemeActiveApi {
|
|
109
|
-
resolveActiveTheme(
|
|
104
|
+
resolveActiveTheme(): Promise<ResolvedTheme>;
|
|
110
105
|
}
|
|
111
106
|
|
|
112
107
|
type ColorScheme = 'auto' | 'light' | 'dark';
|
|
@@ -145,7 +140,7 @@ interface EffectiveThemeConfig {
|
|
|
145
140
|
colorScheme: ColorScheme;
|
|
146
141
|
}
|
|
147
142
|
interface ThemeConfigApi {
|
|
148
|
-
loadThemeConfig(
|
|
143
|
+
loadThemeConfig(): Promise<EffectiveThemeConfig>;
|
|
149
144
|
}
|
|
150
145
|
/**
|
|
151
146
|
* Render `cssVars` as the body of a `:root { ... }` CSS block. Values
|
|
@@ -159,32 +154,42 @@ declare function renderBody(post: Post): string;
|
|
|
159
154
|
* Convert a tiptap doc to its HTML form. Same renderer the public
|
|
160
155
|
* site uses. Defensive: tiptap accepts an HTML string as initial
|
|
161
156
|
* content and parses it on mount, but won't fire onUpdate until the
|
|
162
|
-
* user edits
|
|
157
|
+
* user edits, so a format-switch chain (e.g. markdown -> tiptap ->
|
|
163
158
|
* markdown without editing) can still hand us a raw HTML string
|
|
164
159
|
* here. In that case, return it as-is rather than walking it as a
|
|
165
160
|
* malformed tiptap node and producing empty output.
|
|
166
161
|
*/
|
|
167
162
|
declare function tiptapToHtml(doc: unknown): string;
|
|
168
|
-
/** Convert markdown to HTML using
|
|
163
|
+
/** Convert markdown to HTML using marked + GFM. */
|
|
169
164
|
declare function markdownToHtml(md: string): string;
|
|
170
165
|
/**
|
|
171
166
|
* Walk a tiptap doc and emit Markdown. Mirrors `renderTiptap` in
|
|
172
167
|
* shape but produces markdown syntax. Loses anything markdown can't
|
|
173
168
|
* express (data attributes, image display modes, custom marks).
|
|
174
169
|
*
|
|
170
|
+
* Notes on info loss:
|
|
171
|
+
* - underline / highlight are not in GFM, so they fall back to the
|
|
172
|
+
* literal `<u>` / `<mark>` HTML tags (preserved as-is across round trips).
|
|
173
|
+
* - paragraph / heading textAlign cannot be expressed in markdown and
|
|
174
|
+
* is therefore lost on conversion.
|
|
175
|
+
*
|
|
175
176
|
* Same defensive path as tiptapToHtml: a string input means tiptap
|
|
176
177
|
* hasn't emitted JSON yet (the body is still the HTML we handed it).
|
|
177
178
|
* Route through htmlToMarkdown so the content survives.
|
|
178
179
|
*/
|
|
179
180
|
declare function tiptapToMarkdown(doc: unknown): string;
|
|
180
181
|
/**
|
|
181
|
-
* Regex-based HTML
|
|
182
|
+
* Regex-based HTML -> Markdown converter. Handles the tag set the
|
|
182
183
|
* editor produces (`<p>` `<h1>`-`<h6>` `<strong>` `<em>` `<a>`
|
|
183
184
|
* `<img>` `<ul>` `<ol>` `<li>` `<code>` `<pre>` `<blockquote>` `<hr>`
|
|
184
|
-
* `<br>`
|
|
185
|
-
*
|
|
185
|
+
* `<br>` `<u>` `<mark>` `<table>` task-list `<ul data-type="taskList">`).
|
|
186
|
+
* Decorative containers like `<div style="text-align:...">` are dropped.
|
|
187
|
+
*
|
|
188
|
+
* Tables are reduced to GFM pipe syntax via convertHtmlTable. Complex
|
|
189
|
+
* nested content inside cells (lists, other tables) is flattened to
|
|
190
|
+
* plain text.
|
|
186
191
|
*
|
|
187
|
-
* Not a full library
|
|
192
|
+
* Not a full library, there are known limits like nested formatting
|
|
188
193
|
* inside list items potentially merging. Acceptable for a v0.x
|
|
189
194
|
* format-switch convenience; complex HTML round-trips shouldn't be
|
|
190
195
|
* relied on.
|
|
@@ -198,15 +203,13 @@ interface CreateAmplessOpts {
|
|
|
198
203
|
}
|
|
199
204
|
interface Ampless {
|
|
200
205
|
listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
|
|
201
|
-
getPublishedPost(slug: string
|
|
202
|
-
siteId?: string;
|
|
203
|
-
}): Promise<Post | null>;
|
|
206
|
+
getPublishedPost(slug: string): Promise<Post | null>;
|
|
204
207
|
listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
|
|
205
|
-
loadSiteSettings(
|
|
206
|
-
resolveActiveTheme(
|
|
207
|
-
loadThemeConfig(
|
|
208
|
-
postMetadata(post: Post
|
|
209
|
-
siteMetadata(
|
|
208
|
+
loadSiteSettings(): Promise<EffectiveSiteSettings>;
|
|
209
|
+
resolveActiveTheme(): Promise<ResolvedTheme>;
|
|
210
|
+
loadThemeConfig(): Promise<EffectiveThemeConfig>;
|
|
211
|
+
postMetadata(post: Post): Promise<Metadata>;
|
|
212
|
+
siteMetadata(): Promise<Metadata>;
|
|
210
213
|
renderBody(post: Post): string;
|
|
211
214
|
renderThemeCss(cssVars: Record<string, string>): string;
|
|
212
215
|
publicAssetUrl(key: string): string;
|
package/dist/index.js
CHANGED
|
@@ -20,35 +20,20 @@ function createStorage(outputs) {
|
|
|
20
20
|
// src/posts.ts
|
|
21
21
|
import { cookies } from "next/headers";
|
|
22
22
|
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
|
|
23
|
-
|
|
24
|
-
if (typeof value !== "string") return value;
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(value);
|
|
27
|
-
} catch {
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
23
|
+
import { decodeAwsJson } from "ampless";
|
|
31
24
|
function decodeMetadata(value) {
|
|
32
25
|
if (value === null || value === void 0) return void 0;
|
|
33
|
-
const parsed =
|
|
26
|
+
const parsed = decodeAwsJson(value);
|
|
34
27
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
35
28
|
}
|
|
36
|
-
function safeJsonParse(value) {
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(value);
|
|
39
|
-
} catch {
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
29
|
function toCorePost(p) {
|
|
44
30
|
return {
|
|
45
31
|
postId: p.postId,
|
|
46
|
-
siteId: p.siteId,
|
|
47
32
|
slug: p.slug,
|
|
48
33
|
title: p.title,
|
|
49
34
|
excerpt: p.excerpt ?? void 0,
|
|
50
35
|
format: p.format ?? "markdown",
|
|
51
|
-
body:
|
|
36
|
+
body: decodeAwsJson(p.body),
|
|
52
37
|
status: p.status ?? "draft",
|
|
53
38
|
publishedAt: p.publishedAt ?? void 0,
|
|
54
39
|
tags: (p.tags ?? []).filter((t) => typeof t === "string"),
|
|
@@ -66,7 +51,6 @@ function createPostsApi(outputs) {
|
|
|
66
51
|
return {
|
|
67
52
|
async listPublishedPosts(opts = {}) {
|
|
68
53
|
const { data, errors } = await client.queries.listPublishedPosts({
|
|
69
|
-
siteId: opts.siteId ?? "default",
|
|
70
54
|
from: opts.from,
|
|
71
55
|
to: opts.to,
|
|
72
56
|
limit: opts.limit ?? 20,
|
|
@@ -76,9 +60,8 @@ function createPostsApi(outputs) {
|
|
|
76
60
|
const items = (data?.items ?? []).filter((p) => p !== null).map(toCorePost);
|
|
77
61
|
return { items, nextToken: data?.nextToken ?? null };
|
|
78
62
|
},
|
|
79
|
-
async getPublishedPost(slug
|
|
63
|
+
async getPublishedPost(slug) {
|
|
80
64
|
const { data, errors } = await client.queries.getPublishedPost({
|
|
81
|
-
siteId: opts.siteId ?? "default",
|
|
82
65
|
slug
|
|
83
66
|
});
|
|
84
67
|
if (errors) throw new Error(errors[0]?.message ?? "Failed to get post");
|
|
@@ -86,7 +69,6 @@ function createPostsApi(outputs) {
|
|
|
86
69
|
},
|
|
87
70
|
async listPostsByTag(tag, opts = {}) {
|
|
88
71
|
const { data, errors } = await client.queries.listPostsByTag({
|
|
89
|
-
siteId: opts.siteId ?? "default",
|
|
90
72
|
tag,
|
|
91
73
|
limit: opts.limit ?? 20,
|
|
92
74
|
nextToken: opts.nextToken
|
|
@@ -99,25 +81,27 @@ function createPostsApi(outputs) {
|
|
|
99
81
|
}
|
|
100
82
|
|
|
101
83
|
// src/site-settings.ts
|
|
102
|
-
import {
|
|
84
|
+
import { unflattenSettings } from "ampless";
|
|
103
85
|
function createSiteSettings(cmsConfig, storage) {
|
|
104
|
-
async function fetchRemote(
|
|
86
|
+
async function fetchRemote() {
|
|
105
87
|
if (!storage.isStorageConfigured()) return null;
|
|
106
88
|
let url;
|
|
107
89
|
try {
|
|
108
|
-
url = storage.publicAssetUrl(
|
|
90
|
+
url = storage.publicAssetUrl("public/site-settings.json");
|
|
109
91
|
} catch {
|
|
110
92
|
return null;
|
|
111
93
|
}
|
|
112
|
-
const res = await fetch(url, {
|
|
94
|
+
const res = await fetch(url, {
|
|
95
|
+
next: { revalidate: 60, tags: ["site-settings"] }
|
|
96
|
+
});
|
|
113
97
|
if (!res.ok) return null;
|
|
114
98
|
const flat = await res.json();
|
|
115
99
|
return unflattenSettings(flat);
|
|
116
100
|
}
|
|
117
101
|
return {
|
|
118
|
-
async loadSiteSettings(
|
|
119
|
-
const remote = await fetchRemote(
|
|
120
|
-
const baseSite =
|
|
102
|
+
async loadSiteSettings() {
|
|
103
|
+
const remote = await fetchRemote().catch(() => null);
|
|
104
|
+
const baseSite = cmsConfig.site;
|
|
121
105
|
return {
|
|
122
106
|
site: {
|
|
123
107
|
name: remote?.site?.name ?? baseSite.name,
|
|
@@ -142,15 +126,14 @@ function createSiteSettings(cmsConfig, storage) {
|
|
|
142
126
|
}
|
|
143
127
|
|
|
144
128
|
// src/seo.ts
|
|
145
|
-
import { DEFAULT_SITE_ID as DEFAULT_SITE_ID2 } from "ampless";
|
|
146
129
|
function isPlugin(p) {
|
|
147
130
|
return typeof p === "object" && p !== null && "apiVersion" in p;
|
|
148
131
|
}
|
|
149
132
|
function createSeo(cmsConfig, settingsApi) {
|
|
150
133
|
const plugins = (cmsConfig.plugins ?? []).filter(isPlugin);
|
|
151
134
|
return {
|
|
152
|
-
async postMetadata(post
|
|
153
|
-
const settings = await settingsApi.loadSiteSettings(
|
|
135
|
+
async postMetadata(post) {
|
|
136
|
+
const settings = await settingsApi.loadSiteSettings();
|
|
154
137
|
const accum = {};
|
|
155
138
|
for (const plugin of plugins) {
|
|
156
139
|
if (!plugin.metadata) continue;
|
|
@@ -167,8 +150,8 @@ function createSeo(cmsConfig, settingsApi) {
|
|
|
167
150
|
}
|
|
168
151
|
return accum;
|
|
169
152
|
},
|
|
170
|
-
async siteMetadata(
|
|
171
|
-
const settings = await settingsApi.loadSiteSettings(
|
|
153
|
+
async siteMetadata() {
|
|
154
|
+
const settings = await settingsApi.loadSiteSettings();
|
|
172
155
|
const accum = {
|
|
173
156
|
title: settings.site.name,
|
|
174
157
|
description: settings.site.description
|
|
@@ -193,24 +176,25 @@ function createSeo(cmsConfig, settingsApi) {
|
|
|
193
176
|
|
|
194
177
|
// src/theme-active.ts
|
|
195
178
|
import { headers } from "next/headers";
|
|
196
|
-
import { DEFAULT_SITE_ID as DEFAULT_SITE_ID3 } from "ampless";
|
|
197
179
|
function createThemeActive(registry, storage) {
|
|
198
|
-
async function fetchActiveFromCache(
|
|
180
|
+
async function fetchActiveFromCache() {
|
|
199
181
|
if (!storage.isStorageConfigured()) return null;
|
|
200
182
|
let url;
|
|
201
183
|
try {
|
|
202
|
-
url = storage.publicAssetUrl(
|
|
184
|
+
url = storage.publicAssetUrl("public/site-settings.json");
|
|
203
185
|
} catch {
|
|
204
186
|
return null;
|
|
205
187
|
}
|
|
206
|
-
const res = await fetch(url, {
|
|
188
|
+
const res = await fetch(url, {
|
|
189
|
+
next: { revalidate: 60, tags: ["site-settings"] }
|
|
190
|
+
});
|
|
207
191
|
if (!res.ok) return null;
|
|
208
192
|
const flat = await res.json();
|
|
209
193
|
const v = flat["theme.active"];
|
|
210
194
|
return typeof v === "string" ? v : null;
|
|
211
195
|
}
|
|
212
196
|
return {
|
|
213
|
-
async resolveActiveTheme(
|
|
197
|
+
async resolveActiveTheme() {
|
|
214
198
|
let previewOverride = null;
|
|
215
199
|
try {
|
|
216
200
|
const h = await headers();
|
|
@@ -221,7 +205,7 @@ function createThemeActive(registry, storage) {
|
|
|
221
205
|
const mod2 = registry.themes[previewOverride];
|
|
222
206
|
if (mod2) return { name: previewOverride, module: mod2 };
|
|
223
207
|
}
|
|
224
|
-
const stored = await fetchActiveFromCache(
|
|
208
|
+
const stored = await fetchActiveFromCache().catch(() => null);
|
|
225
209
|
const name = stored && stored in registry.themes ? stored : registry.defaultTheme;
|
|
226
210
|
const mod = registry.themes[name] ?? registry.themes[registry.defaultTheme];
|
|
227
211
|
if (!mod) {
|
|
@@ -236,7 +220,6 @@ function createThemeActive(registry, storage) {
|
|
|
236
220
|
|
|
237
221
|
// src/theme-config.ts
|
|
238
222
|
import {
|
|
239
|
-
DEFAULT_SITE_ID as DEFAULT_SITE_ID4,
|
|
240
223
|
resolveThemeValues,
|
|
241
224
|
themeSettingKey
|
|
242
225
|
} from "ampless";
|
|
@@ -247,23 +230,23 @@ function validateColorScheme(raw) {
|
|
|
247
230
|
return DEFAULT_COLOR_SCHEME;
|
|
248
231
|
}
|
|
249
232
|
function createThemeConfig(themeActive, storage) {
|
|
250
|
-
async function fetchRemote(
|
|
233
|
+
async function fetchRemote() {
|
|
251
234
|
if (!storage.isStorageConfigured()) return null;
|
|
252
235
|
let url;
|
|
253
236
|
try {
|
|
254
|
-
url = storage.publicAssetUrl(
|
|
237
|
+
url = storage.publicAssetUrl("public/site-settings.json");
|
|
255
238
|
} catch {
|
|
256
239
|
return null;
|
|
257
240
|
}
|
|
258
|
-
const res = await fetch(url, { next: { revalidate: 60, tags: [
|
|
241
|
+
const res = await fetch(url, { next: { revalidate: 60, tags: ["site-settings"] } });
|
|
259
242
|
if (!res.ok) return null;
|
|
260
243
|
return await res.json();
|
|
261
244
|
}
|
|
262
245
|
return {
|
|
263
|
-
async loadThemeConfig(
|
|
246
|
+
async loadThemeConfig() {
|
|
264
247
|
const [active, flat] = await Promise.all([
|
|
265
|
-
themeActive.resolveActiveTheme(
|
|
266
|
-
fetchRemote(
|
|
248
|
+
themeActive.resolveActiveTheme(),
|
|
249
|
+
fetchRemote().catch(() => null)
|
|
267
250
|
]);
|
|
268
251
|
const manifest = active.module.manifest;
|
|
269
252
|
const stored = {};
|
|
@@ -299,9 +282,17 @@ ${lines.join("\n")}
|
|
|
299
282
|
}
|
|
300
283
|
|
|
301
284
|
// src/rendering.ts
|
|
285
|
+
import { marked } from "marked";
|
|
302
286
|
function escape(s) {
|
|
303
287
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
304
288
|
}
|
|
289
|
+
function textAlignStyle(attrs) {
|
|
290
|
+
const v = attrs?.textAlign;
|
|
291
|
+
if (v === "left" || v === "center" || v === "right" || v === "justify") {
|
|
292
|
+
return ` style="text-align: ${v}"`;
|
|
293
|
+
}
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
305
296
|
function renderTiptap(node) {
|
|
306
297
|
if (node.type === "text") {
|
|
307
298
|
let html = escape(node.text ?? "");
|
|
@@ -310,6 +301,8 @@ function renderTiptap(node) {
|
|
|
310
301
|
else if (mark.type === "italic") html = `<em>${html}</em>`;
|
|
311
302
|
else if (mark.type === "code") html = `<code>${html}</code>`;
|
|
312
303
|
else if (mark.type === "strike") html = `<s>${html}</s>`;
|
|
304
|
+
else if (mark.type === "underline") html = `<u>${html}</u>`;
|
|
305
|
+
else if (mark.type === "highlight") html = `<mark>${html}</mark>`;
|
|
313
306
|
else if (mark.type === "link") {
|
|
314
307
|
const href = escape(String(mark.attrs?.href ?? "#"));
|
|
315
308
|
html = `<a href="${href}" target="_blank" rel="noopener">${html}</a>`;
|
|
@@ -322,10 +315,10 @@ function renderTiptap(node) {
|
|
|
322
315
|
case "doc":
|
|
323
316
|
return children;
|
|
324
317
|
case "paragraph":
|
|
325
|
-
return `<p>${children}</p>`;
|
|
318
|
+
return `<p${textAlignStyle(node.attrs)}>${children}</p>`;
|
|
326
319
|
case "heading": {
|
|
327
320
|
const level = Number(node.attrs?.level ?? 1);
|
|
328
|
-
return `<h${level}>${children}</h${level}>`;
|
|
321
|
+
return `<h${level}${textAlignStyle(node.attrs)}>${children}</h${level}>`;
|
|
329
322
|
}
|
|
330
323
|
case "bulletList":
|
|
331
324
|
return `<ul>${children}</ul>`;
|
|
@@ -350,52 +343,39 @@ function renderTiptap(node) {
|
|
|
350
343
|
const display = node.attrs?.display ? ` data-display="${escape(String(node.attrs.display))}"` : "";
|
|
351
344
|
return `<img src="${src}" alt="${alt}"${title}${display} loading="lazy" />`;
|
|
352
345
|
}
|
|
346
|
+
case "table":
|
|
347
|
+
return `<table class="tiptap-table"><tbody>${children}</tbody></table>`;
|
|
348
|
+
case "tableRow":
|
|
349
|
+
return `<tr>${children}</tr>`;
|
|
350
|
+
case "tableHeader":
|
|
351
|
+
return `<th${tableCellAttrs(node.attrs)}>${children}</th>`;
|
|
352
|
+
case "tableCell":
|
|
353
|
+
return `<td${tableCellAttrs(node.attrs)}>${children}</td>`;
|
|
354
|
+
case "taskList":
|
|
355
|
+
return `<ul data-type="taskList">${children}</ul>`;
|
|
356
|
+
case "taskItem": {
|
|
357
|
+
const checked = node.attrs?.checked === true ? "true" : "false";
|
|
358
|
+
return `<li data-type="taskItem" data-checked="${checked}">${children}</li>`;
|
|
359
|
+
}
|
|
353
360
|
default:
|
|
354
361
|
return children;
|
|
355
362
|
}
|
|
356
363
|
}
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
out.push(`<h2>${escape(line.slice(3))}</h2>`);
|
|
368
|
-
i++;
|
|
369
|
-
} else if (line.startsWith("```")) {
|
|
370
|
-
const lang = line.slice(3).trim();
|
|
371
|
-
const code = [];
|
|
372
|
-
i++;
|
|
373
|
-
while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
|
|
374
|
-
code.push(lines[i] ?? "");
|
|
375
|
-
i++;
|
|
376
|
-
}
|
|
377
|
-
i++;
|
|
378
|
-
out.push(
|
|
379
|
-
`<pre><code${lang ? ` class="language-${escape(lang)}"` : ""}>${escape(code.join("\n"))}</code></pre>`
|
|
380
|
-
);
|
|
381
|
-
} else if (line.startsWith("- ")) {
|
|
382
|
-
const items = [];
|
|
383
|
-
while (i < lines.length && (lines[i] ?? "").startsWith("- ")) {
|
|
384
|
-
items.push(`<li>${renderInlineMarkdown((lines[i] ?? "").slice(2))}</li>`);
|
|
385
|
-
i++;
|
|
386
|
-
}
|
|
387
|
-
out.push(`<ul>${items.join("")}</ul>`);
|
|
388
|
-
} else if (line.trim() === "") {
|
|
389
|
-
i++;
|
|
390
|
-
} else {
|
|
391
|
-
out.push(`<p>${renderInlineMarkdown(line)}</p>`);
|
|
392
|
-
i++;
|
|
393
|
-
}
|
|
364
|
+
function tableCellAttrs(attrs) {
|
|
365
|
+
let out = "";
|
|
366
|
+
const colspan = Number(attrs?.colspan ?? 1);
|
|
367
|
+
if (colspan > 1) out += ` colspan="${colspan}"`;
|
|
368
|
+
const rowspan = Number(attrs?.rowspan ?? 1);
|
|
369
|
+
if (rowspan > 1) out += ` rowspan="${rowspan}"`;
|
|
370
|
+
const colwidth = attrs?.colwidth;
|
|
371
|
+
if (Array.isArray(colwidth) && colwidth.length > 0) {
|
|
372
|
+
const w = Number(colwidth[0]);
|
|
373
|
+
if (Number.isFinite(w) && w > 0) out += ` style="width: ${w}px"`;
|
|
394
374
|
}
|
|
395
|
-
return out
|
|
375
|
+
return out;
|
|
396
376
|
}
|
|
397
|
-
function
|
|
398
|
-
return
|
|
377
|
+
function renderMarkdown(md) {
|
|
378
|
+
return marked.parse(md, { gfm: true, breaks: false, async: false });
|
|
399
379
|
}
|
|
400
380
|
function renderBody(post) {
|
|
401
381
|
if (post.format === "html") return String(post.body);
|
|
@@ -426,6 +406,8 @@ function tiptapNodeToMarkdown(node) {
|
|
|
426
406
|
else if (mark.type === "italic") txt = `*${txt}*`;
|
|
427
407
|
else if (mark.type === "code") txt = `\`${txt}\``;
|
|
428
408
|
else if (mark.type === "strike") txt = `~~${txt}~~`;
|
|
409
|
+
else if (mark.type === "underline") txt = `<u>${txt}</u>`;
|
|
410
|
+
else if (mark.type === "highlight") txt = `<mark>${txt}</mark>`;
|
|
429
411
|
else if (mark.type === "link") txt = `[${txt}](${String(mark.attrs?.href ?? "#")})`;
|
|
430
412
|
}
|
|
431
413
|
return txt;
|
|
@@ -463,12 +445,54 @@ function tiptapNodeToMarkdown(node) {
|
|
|
463
445
|
const alt = String(node.attrs?.alt ?? "");
|
|
464
446
|
return ``;
|
|
465
447
|
}
|
|
448
|
+
case "table":
|
|
449
|
+
return tiptapTableToMarkdown(node);
|
|
450
|
+
case "taskList":
|
|
451
|
+
return children + "\n";
|
|
452
|
+
case "taskItem": {
|
|
453
|
+
const checked = node.attrs?.checked === true ? "x" : " ";
|
|
454
|
+
const inner = children.replace(/\n+$/, "");
|
|
455
|
+
const [first, ...rest] = inner.split("\n");
|
|
456
|
+
const cont = rest.map((l) => l ? " " + l : l).join("\n");
|
|
457
|
+
return `- [${checked}] ${first ?? ""}${cont ? "\n" + cont : ""}
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
466
460
|
default:
|
|
467
461
|
return children;
|
|
468
462
|
}
|
|
469
463
|
}
|
|
464
|
+
function tiptapTableToMarkdown(node) {
|
|
465
|
+
const rows = node.content ?? [];
|
|
466
|
+
if (rows.length === 0) return "";
|
|
467
|
+
const renderedRows = [];
|
|
468
|
+
let headerIdx = -1;
|
|
469
|
+
for (let i = 0; i < rows.length; i++) {
|
|
470
|
+
const row = rows[i];
|
|
471
|
+
const cells = row.content ?? [];
|
|
472
|
+
const cellTexts = cells.map((c) => {
|
|
473
|
+
const inner = (c.content ?? []).map(tiptapNodeToMarkdown).join("");
|
|
474
|
+
return inner.replace(/\n+$/, "").replace(/\n/g, "<br>").replace(/\|/g, "\\|");
|
|
475
|
+
});
|
|
476
|
+
renderedRows.push(cellTexts);
|
|
477
|
+
if (headerIdx === -1 && cells.some((c) => c.type === "tableHeader")) headerIdx = i;
|
|
478
|
+
}
|
|
479
|
+
if (headerIdx === -1) headerIdx = 0;
|
|
480
|
+
const header = renderedRows[headerIdx] ?? [];
|
|
481
|
+
const body = renderedRows.filter((_, i) => i !== headerIdx);
|
|
482
|
+
const cols = header.length;
|
|
483
|
+
const headerLine = "| " + header.join(" | ") + " |";
|
|
484
|
+
const sepLine = "| " + Array.from({ length: cols }, () => "---").join(" | ") + " |";
|
|
485
|
+
const bodyLines = body.map((r) => {
|
|
486
|
+
const cells = Array.from({ length: cols }, (_, i) => r[i] ?? "");
|
|
487
|
+
return "| " + cells.join(" | ") + " |";
|
|
488
|
+
});
|
|
489
|
+
return "\n" + [headerLine, sepLine, ...bodyLines].join("\n") + "\n\n";
|
|
490
|
+
}
|
|
470
491
|
function htmlToMarkdown(html) {
|
|
471
492
|
let md = html;
|
|
493
|
+
md = md.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, inner) => {
|
|
494
|
+
return "\n" + convertHtmlTable(String(inner)) + "\n";
|
|
495
|
+
});
|
|
472
496
|
md = md.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, text) => {
|
|
473
497
|
return "\n" + "#".repeat(Number(level)) + " " + String(text).trim() + "\n\n";
|
|
474
498
|
});
|
|
@@ -481,6 +505,9 @@ function htmlToMarkdown(html) {
|
|
|
481
505
|
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
482
506
|
return "\n" + String(content).trim().split("\n").map((l) => "> " + l).join("\n") + "\n\n";
|
|
483
507
|
});
|
|
508
|
+
md = md.replace(/<ul[^>]*data-type="taskList"[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
509
|
+
return "\n" + convertHtmlTaskList(String(items)) + "\n";
|
|
510
|
+
});
|
|
484
511
|
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
485
512
|
return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n") + "\n";
|
|
486
513
|
});
|
|
@@ -500,11 +527,63 @@ function htmlToMarkdown(html) {
|
|
|
500
527
|
md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
501
528
|
md = md.replace(/<s>([\s\S]*?)<\/s>/gi, "~~$1~~");
|
|
502
529
|
md = md.replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
530
|
+
const PH_U_OPEN = "AMP_U_OPEN";
|
|
531
|
+
const PH_U_CLOSE = "AMP_U_CLOSE";
|
|
532
|
+
const PH_MARK_OPEN = "AMP_MARK_OPEN";
|
|
533
|
+
const PH_MARK_CLOSE = "AMP_MARK_CLOSE";
|
|
534
|
+
md = md.replace(/<u>([\s\S]*?)<\/u>/gi, `${PH_U_OPEN}$1${PH_U_CLOSE}`);
|
|
535
|
+
md = md.replace(/<mark>([\s\S]*?)<\/mark>/gi, `${PH_MARK_OPEN}$1${PH_MARK_CLOSE}`);
|
|
503
536
|
md = md.replace(/<\/?[^>]+>/g, "");
|
|
537
|
+
md = md.split(PH_U_OPEN).join("<u>").split(PH_U_CLOSE).join("</u>").split(PH_MARK_OPEN).join("<mark>").split(PH_MARK_CLOSE).join("</mark>");
|
|
504
538
|
md = md.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
505
539
|
md = md.replace(/\n{3,}/g, "\n\n");
|
|
506
540
|
return md.trim() + "\n";
|
|
507
541
|
}
|
|
542
|
+
function convertHtmlTable(inner) {
|
|
543
|
+
const stripped = inner.replace(/<\/?(thead|tbody)[^>]*>/gi, "");
|
|
544
|
+
const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
545
|
+
const rows = [];
|
|
546
|
+
let m;
|
|
547
|
+
while ((m = rowRe.exec(stripped)) !== null) {
|
|
548
|
+
const rowHtml = m[1] ?? "";
|
|
549
|
+
const cellRe = /<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
550
|
+
const cells = [];
|
|
551
|
+
let isHeader = false;
|
|
552
|
+
let cm;
|
|
553
|
+
while ((cm = cellRe.exec(rowHtml)) !== null) {
|
|
554
|
+
if ((cm[1] ?? "").toLowerCase() === "th") isHeader = true;
|
|
555
|
+
cells.push(normalizeTableCell(cm[2] ?? ""));
|
|
556
|
+
}
|
|
557
|
+
if (cells.length > 0) rows.push({ isHeader, cells });
|
|
558
|
+
}
|
|
559
|
+
if (rows.length === 0) return "";
|
|
560
|
+
let headerIdx = rows.findIndex((r) => r.isHeader);
|
|
561
|
+
if (headerIdx === -1) headerIdx = 0;
|
|
562
|
+
const header = rows[headerIdx].cells;
|
|
563
|
+
const body = rows.filter((_, i) => i !== headerIdx).map((r) => r.cells);
|
|
564
|
+
const cols = header.length;
|
|
565
|
+
const headerLine = "| " + header.join(" | ") + " |";
|
|
566
|
+
const sepLine = "| " + Array.from({ length: cols }, () => "---").join(" | ") + " |";
|
|
567
|
+
const bodyLines = body.map((cells) => {
|
|
568
|
+
const padded = Array.from({ length: cols }, (_, i) => cells[i] ?? "");
|
|
569
|
+
return "| " + padded.join(" | ") + " |";
|
|
570
|
+
});
|
|
571
|
+
return [headerLine, sepLine, ...bodyLines].join("\n") + "\n";
|
|
572
|
+
}
|
|
573
|
+
function normalizeTableCell(html) {
|
|
574
|
+
return html.replace(/<br\s*\/?>/gi, " ").replace(/<\/?[^>]+>/g, "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
575
|
+
}
|
|
576
|
+
function convertHtmlTaskList(items) {
|
|
577
|
+
const liRe = /<li[^>]*data-checked="(true|false)"[^>]*>([\s\S]*?)<\/li>/gi;
|
|
578
|
+
const out = [];
|
|
579
|
+
let m;
|
|
580
|
+
while ((m = liRe.exec(items)) !== null) {
|
|
581
|
+
const checked = m[1] === "true" ? "x" : " ";
|
|
582
|
+
const inner = String(m[2] ?? "").replace(/<\/?p[^>]*>/gi, "").trim();
|
|
583
|
+
out.push(`- [${checked}] ${inner}`);
|
|
584
|
+
}
|
|
585
|
+
return out.join("\n");
|
|
586
|
+
}
|
|
508
587
|
|
|
509
588
|
// src/index.ts
|
|
510
589
|
function createAmpless(opts) {
|
|
@@ -517,13 +596,13 @@ function createAmpless(opts) {
|
|
|
517
596
|
const themeConfig = createThemeConfig(themeActive, storage);
|
|
518
597
|
return {
|
|
519
598
|
listPublishedPosts: (o) => posts.listPublishedPosts(o),
|
|
520
|
-
getPublishedPost: (slug
|
|
599
|
+
getPublishedPost: (slug) => posts.getPublishedPost(slug),
|
|
521
600
|
listPostsByTag: (tag, o) => posts.listPostsByTag(tag, o),
|
|
522
|
-
loadSiteSettings: (
|
|
523
|
-
resolveActiveTheme: (
|
|
524
|
-
loadThemeConfig: (
|
|
525
|
-
postMetadata: (post
|
|
526
|
-
siteMetadata: (
|
|
601
|
+
loadSiteSettings: () => settings.loadSiteSettings(),
|
|
602
|
+
resolveActiveTheme: () => themeActive.resolveActiveTheme(),
|
|
603
|
+
loadThemeConfig: () => themeConfig.loadThemeConfig(),
|
|
604
|
+
postMetadata: (post) => seo.postMetadata(post),
|
|
605
|
+
siteMetadata: () => seo.siteMetadata(),
|
|
527
606
|
renderBody: (post) => renderBody(post),
|
|
528
607
|
renderThemeCss: (cssVars) => renderThemeCss(cssVars),
|
|
529
608
|
publicAssetUrl: (key) => storage.publicAssetUrl(key),
|
package/dist/middleware.d.ts
CHANGED
|
@@ -8,24 +8,26 @@ type MiddlewareFn = (request: NextRequest) => NextResponse;
|
|
|
8
8
|
/**
|
|
9
9
|
* Build the ampless public-site middleware. Performs:
|
|
10
10
|
*
|
|
11
|
-
* -
|
|
12
|
-
* - `/
|
|
11
|
+
* - `/path` → `/site/default/path` internal rewrite
|
|
12
|
+
* - `/_/<slug>(/...)` → `/site/default/r/<slug>(/...)` rewrite for
|
|
13
|
+
* the unified bare-HTML / static-bundle route
|
|
13
14
|
* - `?previewTheme=<name>` → `x-preview-theme` header forwarding
|
|
14
|
-
* - `Cache-Control: private, no-store` in multi-site mode (Amplify
|
|
15
|
-
* Hosting's CloudFront cache key doesn't include Host, so SSR
|
|
16
|
-
* responses would cross-contaminate at the same path)
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `/
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
16
|
+
* Bare-HTML / static routing is data-driven: the theme post dispatcher
|
|
17
|
+
* redirects `metadata.no_layout` and `format='static'` posts to
|
|
18
|
+
* `/_/<slug>`. The middleware rewrites the public `_` prefix to the
|
|
19
|
+
* routable internal folder name `r/`, because Next.js's App Router
|
|
20
|
+
* excludes any path part starting with `_` during route discovery
|
|
21
|
+
* (see `recursive-readdir` + `ignorePartFilter` in
|
|
22
|
+
* `next/dist/build/route-discovery.js`). The browser URL stays
|
|
23
|
+
* `/_/<slug>(/...)`; only the rewrite target uses `r/`.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Single site per Amplify deployment: the rewritten internal path uses
|
|
26
|
+
* the fixed `default` segment. The internal `/site/[siteId]/` routing
|
|
27
|
+
* tree is retained as an implementation detail; a follow-up PR
|
|
28
|
+
* flattens the URL structure further.
|
|
27
29
|
*/
|
|
28
|
-
declare function createAmplessMiddleware(
|
|
30
|
+
declare function createAmplessMiddleware(_opts: CreateMiddlewareOpts): MiddlewareFn;
|
|
29
31
|
/**
|
|
30
32
|
* Reference matcher config — admin / api / login / static assets /
|
|
31
33
|
* amplify_outputs.json are excluded so middleware doesn't rewrite
|
package/dist/middleware.js
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
// src/middleware.ts
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
|
|
4
|
-
function createAmplessMiddleware(
|
|
5
|
-
const MULTI_SITE = isMultiSite(cmsConfig);
|
|
3
|
+
var INTERNAL_SITE_SEGMENT = "default";
|
|
4
|
+
function createAmplessMiddleware(_opts) {
|
|
6
5
|
return function middleware(request) {
|
|
7
|
-
const host = (request.headers.get("host") ?? "").split(":")[0];
|
|
8
|
-
const siteId = resolveSiteId(host ?? "", cmsConfig);
|
|
9
|
-
if (!siteId) {
|
|
10
|
-
return new NextResponse("Site not found", { status: 404 });
|
|
11
|
-
}
|
|
12
6
|
const url = request.nextUrl.clone();
|
|
13
7
|
if (!url.pathname.startsWith("/site/")) {
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
let tail = url.pathname === "/" ? "" : url.pathname;
|
|
9
|
+
if (tail === "/_" || tail.startsWith("/_/")) {
|
|
10
|
+
tail = `/r${tail.slice(2)}`;
|
|
11
|
+
}
|
|
12
|
+
url.pathname = `/site/${INTERNAL_SITE_SEGMENT}${tail}`;
|
|
16
13
|
}
|
|
17
14
|
const previewTheme = url.searchParams.get("previewTheme");
|
|
18
15
|
const previewColorScheme = url.searchParams.get("previewColorScheme");
|
|
19
16
|
const requestHeaders = new Headers(request.headers);
|
|
20
|
-
requestHeaders.set("x-site-id", siteId);
|
|
21
17
|
if (previewTheme) requestHeaders.set("x-preview-theme", previewTheme);
|
|
22
18
|
if (previewColorScheme) requestHeaders.set("x-preview-color-scheme", previewColorScheme);
|
|
23
|
-
|
|
24
|
-
response.headers.set("x-site-id", siteId);
|
|
25
|
-
if (MULTI_SITE) {
|
|
26
|
-
response.headers.set("Cache-Control", "private, no-store");
|
|
27
|
-
}
|
|
28
|
-
return response;
|
|
19
|
+
return NextResponse.rewrite(url, { request: { headers: requestHeaders } });
|
|
29
20
|
};
|
|
30
21
|
}
|
|
31
22
|
var defaultMatcherConfig = {
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -2,112 +2,86 @@ import { Ampless } from '../index.js';
|
|
|
2
2
|
import 'ampless';
|
|
3
3
|
import 'next';
|
|
4
4
|
|
|
5
|
-
interface Ctx$
|
|
5
|
+
interface Ctx$3 {
|
|
6
6
|
params: Promise<{
|
|
7
7
|
siteId: string;
|
|
8
8
|
slug: string;
|
|
9
9
|
}>;
|
|
10
10
|
}
|
|
11
|
-
type OgRouteHandler = (req: Request, ctx: Ctx$
|
|
11
|
+
type OgRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
|
|
12
12
|
declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
|
|
13
13
|
|
|
14
|
-
interface Ctx$
|
|
14
|
+
interface Ctx$2 {
|
|
15
15
|
params: Promise<{
|
|
16
16
|
siteId: string;
|
|
17
17
|
}>;
|
|
18
18
|
}
|
|
19
|
-
type SitemapRouteHandler = (req: Request, ctx: Ctx$
|
|
19
|
+
type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
20
20
|
/**
|
|
21
|
-
* Sitemap route delegate. Looks up the active theme
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Sitemap route delegate. Looks up the active theme and forwards to
|
|
22
|
+
* whichever `routes.sitemap` handler the theme provides. Themes
|
|
23
|
+
* without a sitemap handler return 404.
|
|
24
24
|
*/
|
|
25
25
|
declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
|
|
26
26
|
|
|
27
|
-
interface Ctx$
|
|
27
|
+
interface Ctx$1 {
|
|
28
28
|
params: Promise<{
|
|
29
29
|
siteId: string;
|
|
30
30
|
}>;
|
|
31
31
|
}
|
|
32
|
-
type FeedRouteHandler = (req: Request, ctx: Ctx$
|
|
32
|
+
type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
33
33
|
/**
|
|
34
|
-
* Feed route delegate. Looks up the active theme
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Feed route delegate. Looks up the active theme and forwards to
|
|
35
|
+
* whichever `routes.feed` handler the theme provides. Themes without
|
|
36
|
+
* a feed handler return 404.
|
|
37
37
|
*/
|
|
38
38
|
declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
|
|
39
39
|
|
|
40
|
-
interface Ctx
|
|
40
|
+
interface Ctx {
|
|
41
41
|
params: Promise<{
|
|
42
|
-
siteId: string;
|
|
43
42
|
slug: string;
|
|
43
|
+
path?: string[];
|
|
44
44
|
}>;
|
|
45
45
|
}
|
|
46
|
-
type
|
|
46
|
+
type UnderscoreRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
* entire HTTP response — no Next.js root layout, no theme chrome.
|
|
50
|
-
*
|
|
51
|
-
* Reached via the data-driven dispatcher path: the theme post page
|
|
52
|
-
* checks `post.metadata.no_layout` and redirects to `/raw/<slug>` when
|
|
53
|
-
* set. Posts without that flag never hit this route in practice; the
|
|
54
|
-
* handler additionally enforces the same check here so a direct
|
|
55
|
-
* `/raw/<slug>` request can't bypass the theme for a post that wasn't
|
|
56
|
-
* marked no-layout (would otherwise surface the body without any
|
|
57
|
-
* chrome the editor didn't intend).
|
|
48
|
+
* Unified `/_/<slug>(/...)` route handler.
|
|
58
49
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* page can't replace the document. A `route.ts` returns a Response
|
|
62
|
-
* directly and bypasses React rendering entirely.
|
|
50
|
+
* The public URL prefix `/_/` is reserved for two related cases that
|
|
51
|
+
* both need to bypass the theme's post page:
|
|
63
52
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* about those, use `format: 'html'`.
|
|
72
|
-
*
|
|
73
|
-
* Trust model: the response body is the editor's content verbatim,
|
|
74
|
-
* with no sanitization. Same trust assumption as `format: 'html'`
|
|
75
|
-
* post bodies on the regular path — see
|
|
76
|
-
* docs/architecture/04-access-layer-mcp.md §"editor の信頼モデル".
|
|
77
|
-
*/
|
|
78
|
-
declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
|
|
79
|
-
|
|
80
|
-
interface Ctx {
|
|
81
|
-
params: Promise<{
|
|
82
|
-
siteId: string;
|
|
83
|
-
path: string[];
|
|
84
|
-
}>;
|
|
85
|
-
}
|
|
86
|
-
type StaticRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
87
|
-
/**
|
|
88
|
-
* Catch-all route for static bundles. Lives at
|
|
89
|
-
* `app/site/[siteId]/[...path]/route.ts` and serves every file
|
|
90
|
-
* inside a `format: 'static'` post's bundle. The first segment of
|
|
91
|
-
* `path` is the post slug; the rest is the relative path inside the
|
|
92
|
-
* bundle (or empty, in which case the manifest's entrypoint is used).
|
|
53
|
+
* 1. `format: 'html'` posts with `metadata.no_layout === true` — the
|
|
54
|
+
* body is its own complete HTML document and ships as the entire
|
|
55
|
+
* response. URL: `/_/<slug>` (no trailing slash, no extra path).
|
|
56
|
+
* 2. `format: 'static'` posts — the body is a manifest describing a
|
|
57
|
+
* bundle of files in S3 at `public/static/<slug>/`. The bundle's
|
|
58
|
+
* entrypoint is served at `/_/<slug>/`, every internal file at
|
|
59
|
+
* `/_/<slug>/<relative-path>`.
|
|
93
60
|
*
|
|
94
|
-
* Routing
|
|
95
|
-
* `[siteId]/[slug]/
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
61
|
+
* Routing model:
|
|
62
|
+
* - File location: `app/site/[siteId]/r/[slug]/[[...path]]/route.ts`.
|
|
63
|
+
* The literal folder is `r/` (not `_/`) because Next.js's App
|
|
64
|
+
* Router skips any path part starting with `_` during route
|
|
65
|
+
* discovery (see `recursive-readdir` + `ignorePartFilter` in
|
|
66
|
+
* next/dist/build/route-discovery.js). Middleware rewrites the
|
|
67
|
+
* public `/_/` prefix to `/r/` internally; the public URL stays
|
|
68
|
+
* `/_/<slug>(/...)`.
|
|
69
|
+
* - `params.path` is `undefined` (or `[]`) for single-segment
|
|
70
|
+
* requests `/_/<slug>`, an array of remaining segments otherwise.
|
|
99
71
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
72
|
+
* Trailing-slash responsibility lives here, not in the dispatcher:
|
|
73
|
+
* the dispatcher redirects `format='static'` posts to `/_/<slug>`,
|
|
74
|
+
* and this handler then 308-redirects to `/_/<slug>/` to anchor
|
|
75
|
+
* relative paths inside the bundle. Same anchoring reason as the
|
|
76
|
+
* legacy static route's behaviour — `<img src="img.png">` must resolve
|
|
77
|
+
* to `/_/<slug>/img.png`, not the site root.
|
|
103
78
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* fresh.
|
|
79
|
+
* Trust model: no_layout HTML bodies are emitted verbatim, same trust
|
|
80
|
+
* shape as `format: 'html'` post bodies on the regular path. Static
|
|
81
|
+
* bundle assets are served via short-lived S3 presigned URLs; the
|
|
82
|
+
* bucket stays private. See docs/architecture/04-access-layer-mcp.md
|
|
83
|
+
* §"editor の信頼モデル".
|
|
110
84
|
*/
|
|
111
|
-
declare function
|
|
85
|
+
declare function createUnderscoreRouteHandler(ampless: Ampless): UnderscoreRouteHandler;
|
|
112
86
|
|
|
113
|
-
export { type FeedRouteHandler, type OgRouteHandler, type
|
|
87
|
+
export { type FeedRouteHandler, type OgRouteHandler, type SitemapRouteHandler, type UnderscoreRouteHandler, createFeedRouteHandler, createOgRouteHandler, createSitemapRouteHandler, createUnderscoreRouteHandler };
|
package/dist/routes/index.js
CHANGED
|
@@ -6,13 +6,13 @@ function hasOgImage(p) {
|
|
|
6
6
|
}
|
|
7
7
|
function createOgRouteHandler(ampless) {
|
|
8
8
|
return async function GET(_req, { params }) {
|
|
9
|
-
const {
|
|
9
|
+
const { slug } = await params;
|
|
10
10
|
const cleanSlug = slug.replace(/\.png$/, "");
|
|
11
|
-
const post = await ampless.getPublishedPost(cleanSlug
|
|
11
|
+
const post = await ampless.getPublishedPost(cleanSlug);
|
|
12
12
|
if (!post) return new Response("not found", { status: 404 });
|
|
13
13
|
const plugin = (ampless.cmsConfig.plugins ?? []).find(hasOgImage);
|
|
14
14
|
if (!plugin) return new Response("og not configured", { status: 404 });
|
|
15
|
-
const settings = await ampless.loadSiteSettings(
|
|
15
|
+
const settings = await ampless.loadSiteSettings();
|
|
16
16
|
const fonts = await Promise.all(
|
|
17
17
|
plugin.ogImage.fonts.map(async (f) => ({
|
|
18
18
|
name: f.name,
|
|
@@ -43,122 +43,127 @@ function createOgRouteHandler(ampless) {
|
|
|
43
43
|
|
|
44
44
|
// src/routes/sitemap.ts
|
|
45
45
|
function createSitemapRouteHandler(ampless) {
|
|
46
|
-
return async function GET(request
|
|
47
|
-
const {
|
|
48
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
46
|
+
return async function GET(request) {
|
|
47
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
49
48
|
const handler = module.routes?.sitemap;
|
|
50
49
|
if (!handler) {
|
|
51
50
|
return new Response("sitemap not implemented for this theme", { status: 404 });
|
|
52
51
|
}
|
|
53
|
-
return handler({
|
|
52
|
+
return handler({ request });
|
|
54
53
|
};
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
// src/routes/feed.ts
|
|
58
57
|
function createFeedRouteHandler(ampless) {
|
|
59
|
-
return async function GET(request
|
|
60
|
-
const {
|
|
61
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
58
|
+
return async function GET(request) {
|
|
59
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
62
60
|
const handler = module.routes?.feed;
|
|
63
61
|
if (!handler) {
|
|
64
62
|
return new Response("feed not implemented for this theme", { status: 404 });
|
|
65
63
|
}
|
|
66
|
-
return handler({
|
|
64
|
+
return handler({ request });
|
|
67
65
|
};
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
// src/routes/
|
|
71
|
-
function createRawRouteHandler(ampless) {
|
|
72
|
-
return async function GET(_request, { params }) {
|
|
73
|
-
const { siteId, slug } = await params;
|
|
74
|
-
const post = await ampless.getPublishedPost(slug, { siteId });
|
|
75
|
-
if (!post) {
|
|
76
|
-
return new Response("Not Found", { status: 404 });
|
|
77
|
-
}
|
|
78
|
-
if (post.metadata?.no_layout !== true) {
|
|
79
|
-
return new Response("Not Found", { status: 404 });
|
|
80
|
-
}
|
|
81
|
-
return new Response(ampless.renderBody(post), {
|
|
82
|
-
status: 200,
|
|
83
|
-
headers: {
|
|
84
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
85
|
-
"Cache-Control": "public, max-age=300"
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// src/routes/static.ts
|
|
68
|
+
// src/routes/underscore.ts
|
|
92
69
|
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
|
|
93
70
|
import { cookies } from "next/headers";
|
|
94
71
|
import { getUrl } from "aws-amplify/storage/server";
|
|
95
|
-
|
|
96
|
-
".html": "text/html; charset=utf-8",
|
|
97
|
-
".htm": "text/html; charset=utf-8"
|
|
98
|
-
};
|
|
99
|
-
function createStaticRouteHandler(ampless) {
|
|
72
|
+
function createUnderscoreRouteHandler(ampless) {
|
|
100
73
|
const { runWithAmplifyServerContext } = createServerRunner({
|
|
101
74
|
config: ampless.outputs
|
|
102
75
|
});
|
|
103
76
|
return async function GET(request, { params }) {
|
|
104
|
-
const {
|
|
105
|
-
|
|
77
|
+
const { slug, path } = await params;
|
|
78
|
+
const restSegments = path ?? [];
|
|
79
|
+
const post = await ampless.getPublishedPost(slug);
|
|
80
|
+
if (!post) {
|
|
81
|
+
return new Response("Not Found", { status: 404 });
|
|
82
|
+
}
|
|
83
|
+
if (restSegments.length === 0) {
|
|
84
|
+
if (post.format === "html" && post.metadata?.no_layout === true) {
|
|
85
|
+
return new Response(ampless.renderBody(post), {
|
|
86
|
+
status: 200,
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
89
|
+
"Cache-Control": "public, max-age=300"
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (post.format === "static") {
|
|
94
|
+
const url = new URL(request.url);
|
|
95
|
+
if (!url.pathname.endsWith("/")) {
|
|
96
|
+
const next = new URL(url);
|
|
97
|
+
next.pathname = `${url.pathname}/`;
|
|
98
|
+
return Response.redirect(next.toString(), 308);
|
|
99
|
+
}
|
|
100
|
+
const body2 = post.body ?? null;
|
|
101
|
+
const entrypoint = typeof body2?.entrypoint === "string" && body2.entrypoint ? body2.entrypoint : "index.html";
|
|
102
|
+
const presignedUrl2 = await signStaticAsset({
|
|
103
|
+
runWithAmplifyServerContext,
|
|
104
|
+
slug,
|
|
105
|
+
rest: entrypoint
|
|
106
|
+
});
|
|
107
|
+
if (!presignedUrl2) {
|
|
108
|
+
return new Response("Not Found", { status: 404 });
|
|
109
|
+
}
|
|
110
|
+
return Response.redirect(presignedUrl2, 302);
|
|
111
|
+
}
|
|
112
|
+
return new Response("Not Found", { status: 404 });
|
|
113
|
+
}
|
|
114
|
+
if (post.format !== "static") {
|
|
106
115
|
return new Response("Not Found", { status: 404 });
|
|
107
116
|
}
|
|
108
|
-
const slug = path[0];
|
|
109
|
-
const restSegments = path.slice(1);
|
|
110
117
|
if (restSegments.some(
|
|
111
118
|
(s) => !s || s === "." || s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0")
|
|
112
119
|
)) {
|
|
113
120
|
return new Response("Invalid path", { status: 400 });
|
|
114
121
|
}
|
|
115
|
-
const post = await ampless.getPublishedPost(slug, { siteId });
|
|
116
|
-
if (!post || post.format !== "static") {
|
|
117
|
-
return new Response("Not Found", { status: 404 });
|
|
118
|
-
}
|
|
119
122
|
const body = post.body ?? null;
|
|
120
|
-
const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
|
|
121
123
|
const fileList = Array.isArray(body?.files) ? body.files : [];
|
|
122
|
-
|
|
123
|
-
if (rest === "") {
|
|
124
|
-
const url = new URL(request.url);
|
|
125
|
-
if (!url.pathname.endsWith("/")) {
|
|
126
|
-
const next = new URL(url);
|
|
127
|
-
next.pathname = `${url.pathname}/`;
|
|
128
|
-
return Response.redirect(next.toString(), 308);
|
|
129
|
-
}
|
|
130
|
-
rest = entrypoint;
|
|
131
|
-
}
|
|
124
|
+
const rest = restSegments.join("/");
|
|
132
125
|
if (fileList.length > 0 && !fileList.includes(rest)) {
|
|
133
126
|
return new Response("Not Found", { status: 404 });
|
|
134
127
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
options: { expiresIn: 60 * 60 }
|
|
143
|
-
});
|
|
144
|
-
return result.url.toString();
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
return Response.redirect(presignedUrl, 302);
|
|
148
|
-
} catch {
|
|
149
|
-
const ext = rest.slice(rest.lastIndexOf("."));
|
|
150
|
-
const fallback = FALLBACK_MIME[ext] ?? "text/plain; charset=utf-8";
|
|
151
|
-
return new Response("Not Found", {
|
|
152
|
-
status: 404,
|
|
153
|
-
headers: { "Content-Type": fallback }
|
|
154
|
-
});
|
|
128
|
+
const presignedUrl = await signStaticAsset({
|
|
129
|
+
runWithAmplifyServerContext,
|
|
130
|
+
slug,
|
|
131
|
+
rest
|
|
132
|
+
});
|
|
133
|
+
if (!presignedUrl) {
|
|
134
|
+
return new Response("Not Found", { status: 404 });
|
|
155
135
|
}
|
|
136
|
+
return Response.redirect(presignedUrl, 302);
|
|
156
137
|
};
|
|
157
138
|
}
|
|
139
|
+
async function signStaticAsset({
|
|
140
|
+
runWithAmplifyServerContext,
|
|
141
|
+
slug,
|
|
142
|
+
rest
|
|
143
|
+
}) {
|
|
144
|
+
const objectPath = `public/static/${slug}/${rest}`;
|
|
145
|
+
try {
|
|
146
|
+
return await runWithAmplifyServerContext({
|
|
147
|
+
nextServerContext: { cookies },
|
|
148
|
+
operation: async (amplifyContext) => {
|
|
149
|
+
const result = await getUrl(amplifyContext, {
|
|
150
|
+
path: objectPath,
|
|
151
|
+
options: { expiresIn: 60 * 60 }
|
|
152
|
+
});
|
|
153
|
+
return result.url.toString();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(
|
|
158
|
+
`[underscore-route] presign failed for ${objectPath}:`,
|
|
159
|
+
err
|
|
160
|
+
);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
158
164
|
export {
|
|
159
165
|
createFeedRouteHandler,
|
|
160
166
|
createOgRouteHandler,
|
|
161
|
-
createRawRouteHandler,
|
|
162
167
|
createSitemapRouteHandler,
|
|
163
|
-
|
|
168
|
+
createUnderscoreRouteHandler
|
|
164
169
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,9 +47,10 @@
|
|
|
47
47
|
"class-variance-authority": "^0.7.1",
|
|
48
48
|
"clsx": "^2.1.1",
|
|
49
49
|
"lucide-react": "^1.16.0",
|
|
50
|
+
"marked": "^14.1.4",
|
|
50
51
|
"tailwind-merge": "^3.6.0",
|
|
51
|
-
"ampless": "0.
|
|
52
|
-
"@ampless/plugin-og-image": "0.2.0-alpha.
|
|
52
|
+
"ampless": "1.0.0-alpha.9",
|
|
53
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.9"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"next": "^15 || ^16",
|