@ampless/runtime 1.0.0-beta.67 → 1.0.0-beta.69
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 +4 -1
- package/README.md +4 -1
- package/dist/index.d.ts +90 -2
- package/dist/index.js +107 -1
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.js +12 -4
- package/dist/routes/index.d.ts +33 -11
- package/dist/routes/index.js +23 -0
- package/package.json +3 -3
package/README.ja.md
CHANGED
|
@@ -92,6 +92,9 @@ middleware はリクエストごとに AppSync から `post.format` /
|
|
|
92
92
|
`app/raw/[slug]/route.ts` で配信
|
|
93
93
|
- `format: 'static'` → `/static/<slug>(/<path>)`、
|
|
94
94
|
`app/static/[slug]/[[...path]]/route.ts` で配信
|
|
95
|
+
- `/<slug>.md`(フォーマット不問) → `/md/<slug>`、
|
|
96
|
+
`app/md/[slug]/route.ts` で配信 — `ampless.postToMarkdown()` による
|
|
97
|
+
Markdown 投影。`cms.config.ai.markdownRoutes: false` で無効化できます。
|
|
95
98
|
|
|
96
99
|
また、`post.metadata.cache`(auto / deep / hot)+ `post.updatedAt` +
|
|
97
100
|
`cms.config.cache.{cooldownMs, freshTtlSeconds, deepTtlSeconds}` から
|
|
@@ -102,7 +105,7 @@ middleware はリクエストごとに AppSync から `post.format` /
|
|
|
102
105
|
|
|
103
106
|
- `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
|
|
104
107
|
- `@ampless/runtime/middleware` — `createAmplessMiddleware`、`defaultMatcherConfig`
|
|
105
|
-
- `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`、`createStaticRouteHandler`
|
|
108
|
+
- `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`、`createStaticRouteHandler`、`createMarkdownRouteHandler`
|
|
106
109
|
- `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`、`createThemePostDispatcher`、`createThemeTagDispatcher`(それぞれ対応する `*Metadata` ファクトリーあり)
|
|
107
110
|
|
|
108
111
|
## テンプレートに残るもの
|
package/README.md
CHANGED
|
@@ -92,6 +92,9 @@ rewrites the request to the right internal handler:
|
|
|
92
92
|
`app/raw/[slug]/route.ts`
|
|
93
93
|
- `format: 'static'` → `/static/<slug>(/<path>)`, served by
|
|
94
94
|
`app/static/[slug]/[[...path]]/route.ts`
|
|
95
|
+
- `/<slug>.md` (any format) → `/md/<slug>`, served by
|
|
96
|
+
`app/md/[slug]/route.ts` — Markdown projection via
|
|
97
|
+
`ampless.postToMarkdown()`. Disable with `cms.config.ai.markdownRoutes: false`.
|
|
95
98
|
|
|
96
99
|
It also computes `Cache-Control` from `post.metadata.cache` (auto /
|
|
97
100
|
deep / hot) + `post.updatedAt` + `cms.config.cache.{cooldownMs,
|
|
@@ -102,7 +105,7 @@ See `docs/CONTENT.md` for the cache strategy contract.
|
|
|
102
105
|
|
|
103
106
|
- `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
|
|
104
107
|
- `@ampless/runtime/middleware` — `createAmplessMiddleware`, `defaultMatcherConfig`
|
|
105
|
-
- `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`, `createStaticRouteHandler`
|
|
108
|
+
- `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`, `createStaticRouteHandler`, `createMarkdownRouteHandler`
|
|
106
109
|
- `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`, `createThemePostDispatcher`, `createThemeTagDispatcher` (each with a matching `*Metadata` factory)
|
|
107
110
|
|
|
108
111
|
## What's still in the template
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Post, MediaMetadata, AmplessPlugin, ContentFieldRenderer, PluginPublicRenderContext, TiptapNodeHtmlAdapters,
|
|
1
|
+
import { Post, MediaMetadata, AmplessPlugin, ContentFieldRenderer, TiptapNodeMarkdownAdapters, PluginPublicRenderContext, TiptapNodeHtmlAdapters, Config, ThemeModule, ThemeManifest, PluginPackageManifest } from 'ampless';
|
|
2
2
|
export { Config, Post, ThemeManifest } from 'ampless';
|
|
3
3
|
import { Metadata } from 'next';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
@@ -188,6 +188,21 @@ interface ContentFieldRegistry {
|
|
|
188
188
|
* render that happens to walk the conflicting node.
|
|
189
189
|
*/
|
|
190
190
|
declare function buildContentFieldRegistry(plugins: readonly AmplessPlugin[]): ContentFieldRegistry;
|
|
191
|
+
/**
|
|
192
|
+
* Merge every plugin's server-safe `tiptapNodeToMarkdown` map into a
|
|
193
|
+
* single `TiptapNodeMarkdownAdapters` registry. Eagerly errors on
|
|
194
|
+
* duplicate `nodeType` registration across plugins (two different
|
|
195
|
+
* functions) so the misuse surfaces at config time, not at the first
|
|
196
|
+
* markdown conversion that happens to walk the conflicting node. The
|
|
197
|
+
* exact same function registered twice (e.g. one shared adapter map
|
|
198
|
+
* reused by two plugin instances) is tolerated.
|
|
199
|
+
*
|
|
200
|
+
* Non-function entries are skipped with a warning rather than thrown:
|
|
201
|
+
* `definePlugin` already warns about them at definition time, and one
|
|
202
|
+
* broken plugin shouldn't take the whole site's markdown output down
|
|
203
|
+
* (a nodeType conflict, by contrast, is a config error — that throws).
|
|
204
|
+
*/
|
|
205
|
+
declare function buildMarkdownAdapterRegistry(plugins: readonly AmplessPlugin[]): TiptapNodeMarkdownAdapters;
|
|
191
206
|
interface RenderBodyOptions {
|
|
192
207
|
contentFields?: ContentFieldRegistry;
|
|
193
208
|
/**
|
|
@@ -280,6 +295,54 @@ declare function tiptapToMarkdown(doc: unknown, opts?: {
|
|
|
280
295
|
* relied on.
|
|
281
296
|
*/
|
|
282
297
|
declare function htmlToMarkdown(html: string): string;
|
|
298
|
+
interface PostToMarkdownOptions {
|
|
299
|
+
/**
|
|
300
|
+
* Per-nodeType tiptap→markdown serialisers (plugin embed nodes →
|
|
301
|
+
* bare URL lines). `createAmpless` injects
|
|
302
|
+
* `pluginHead.markdownAdapters`; a raw direct caller can omit this
|
|
303
|
+
* to fall back to the built-in switch (unknown atom nodes then emit
|
|
304
|
+
* unsupported-node placeholder comments).
|
|
305
|
+
*/
|
|
306
|
+
nodeAdapters?: TiptapNodeMarkdownAdapters;
|
|
307
|
+
/** Prepend YAML frontmatter (default: true). */
|
|
308
|
+
frontmatter?: boolean;
|
|
309
|
+
/**
|
|
310
|
+
* Absolute site origin used to build the frontmatter's `canonical`
|
|
311
|
+
* line (`<siteUrl>/<slug>`). Omitted → no canonical line.
|
|
312
|
+
*/
|
|
313
|
+
siteUrl?: string;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Convert a public post into its canonical Markdown representation:
|
|
317
|
+
* YAML frontmatter + format-dependent body. This is the single
|
|
318
|
+
* conversion point for every AI-readable surface (the `/<slug>.md`
|
|
319
|
+
* route, llms-full, public MCP `get_post`) — new surfaces should call
|
|
320
|
+
* this rather than reimplementing the format branches.
|
|
321
|
+
*
|
|
322
|
+
* Frontmatter carries **public fields only** (title / slug /
|
|
323
|
+
* publishedAt / updatedAt / tags / excerpt / canonical) — never
|
|
324
|
+
* postId, status, or metadata. Values are encoded with
|
|
325
|
+
* `JSON.stringify`: a JSON string is a valid YAML double-quoted
|
|
326
|
+
* scalar and a JSON array is a valid YAML flow sequence, so quoting /
|
|
327
|
+
* escaping is correct without a YAML library.
|
|
328
|
+
*
|
|
329
|
+
* Body quality varies by format:
|
|
330
|
+
* - `markdown`: the stored source, verbatim.
|
|
331
|
+
* - `tiptap`: `tiptapToMarkdown` with `opts.nodeAdapters` (plugin
|
|
332
|
+
* embeds become bare URL lines; unknown atom nodes emit an
|
|
333
|
+
* `ampless:unsupported-node` placeholder comment).
|
|
334
|
+
* - `html`: `htmlToMarkdown` — regex-based and approximate; complex
|
|
335
|
+
* markup may not fully survive (see htmlToMarkdown's limits).
|
|
336
|
+
* - `static`: no body conversion (the bundle bytes live in S3) — a
|
|
337
|
+
* markdown link to the served entrypoint, plus the excerpt when
|
|
338
|
+
* present.
|
|
339
|
+
*
|
|
340
|
+
* Pure, synchronous, and node-safe — same tier as `tiptapToMarkdown` /
|
|
341
|
+
* `htmlToMarkdown`. Themes / routes should usually call it through
|
|
342
|
+
* `ampless.postToMarkdown(post)`, which injects the plugin adapter
|
|
343
|
+
* registry and the effective site URL.
|
|
344
|
+
*/
|
|
345
|
+
declare function postToMarkdown(post: Post, opts?: PostToMarkdownOptions): string;
|
|
283
346
|
|
|
284
347
|
interface PluginHeadApi {
|
|
285
348
|
/**
|
|
@@ -328,6 +391,15 @@ interface PluginHeadApi {
|
|
|
328
391
|
* `contentFields`.
|
|
329
392
|
*/
|
|
330
393
|
readonly contentFieldsRegistry: ContentFieldRegistry;
|
|
394
|
+
/**
|
|
395
|
+
* Merged tiptap→markdown adapter registry across all valid plugins
|
|
396
|
+
* (server-safe `tiptapNodeToMarkdown` manifests). Built once at
|
|
397
|
+
* `createPluginHead` construction time so duplicate `nodeType`
|
|
398
|
+
* registrations across plugins throw eagerly (config error).
|
|
399
|
+
* Threaded into `rendering.ts:postToMarkdown()` via
|
|
400
|
+
* `Ampless.postToMarkdown`.
|
|
401
|
+
*/
|
|
402
|
+
readonly markdownAdapters: TiptapNodeMarkdownAdapters;
|
|
331
403
|
/**
|
|
332
404
|
* Resolve the per-plugin `PluginPublicRenderContext` snapshot used by
|
|
333
405
|
* `contentFields` renderers. Same `settings()` binding the public
|
|
@@ -652,6 +724,22 @@ interface Ampless {
|
|
|
652
724
|
* complete HTML documents that don't expand embed shortcuts.
|
|
653
725
|
*/
|
|
654
726
|
renderBodyHtmlString(post: Post): string;
|
|
727
|
+
/**
|
|
728
|
+
* Convert a post into its canonical Markdown representation (YAML
|
|
729
|
+
* frontmatter + body). This is the **single conversion point** for
|
|
730
|
+
* every AI-readable surface — the `/<slug>.md` route, llms-full, and
|
|
731
|
+
* the public MCP `get_post` all go through here rather than
|
|
732
|
+
* reimplementing the format branches. Plugin embed nodes are
|
|
733
|
+
* serialised via `pluginHead.markdownAdapters` (bare URL lines);
|
|
734
|
+
* `format: 'html'` bodies go through the regex-based `htmlToMarkdown`
|
|
735
|
+
* and are approximate. Async because the frontmatter's `canonical`
|
|
736
|
+
* line needs the effective `site.url` from the S3 site-settings
|
|
737
|
+
* cache; with `frontmatter: false` that read is skipped entirely and
|
|
738
|
+
* only the body is returned.
|
|
739
|
+
*/
|
|
740
|
+
postToMarkdown(post: Post, opts?: {
|
|
741
|
+
frontmatter?: boolean;
|
|
742
|
+
}): Promise<string>;
|
|
655
743
|
renderThemeCss(cssVars: Record<string, string>): string;
|
|
656
744
|
publicAssetUrl(key: string): string;
|
|
657
745
|
isStorageConfigured(): boolean;
|
|
@@ -677,4 +765,4 @@ interface Ampless {
|
|
|
677
765
|
*/
|
|
678
766
|
declare function createAmpless(opts: CreateAmplessOpts): Ampless;
|
|
679
767
|
|
|
680
|
-
export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type ContentFieldRegistry, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PluginSettingsApi, type PluginSettingsSnapshot, type PostsApi, type PublicHtmlForPostResult, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type RenderBodyOptions, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, SUPPORTED_API_VERSION, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, buildContentFieldRegistry, createAmpless, createPluginHead, createPluginSettings, escapeJsonLdInlineBody, htmlToMarkdown, loadPackageManifest, markdownToHtml, renderBody, renderBodyHtmlString, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
|
|
768
|
+
export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type ContentFieldRegistry, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PluginSettingsApi, type PluginSettingsSnapshot, type PostToMarkdownOptions, type PostsApi, type PublicHtmlForPostResult, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type RenderBodyOptions, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, SUPPORTED_API_VERSION, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, buildContentFieldRegistry, buildMarkdownAdapterRegistry, createAmpless, createPluginHead, createPluginSettings, escapeJsonLdInlineBody, htmlToMarkdown, loadPackageManifest, markdownToHtml, postToMarkdown, renderBody, renderBodyHtmlString, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
|
package/dist/index.js
CHANGED
|
@@ -399,6 +399,29 @@ function buildContentFieldRegistry(plugins) {
|
|
|
399
399
|
}
|
|
400
400
|
return { tiptap, markdownUrl, htmlPlaceholder };
|
|
401
401
|
}
|
|
402
|
+
function buildMarkdownAdapterRegistry(plugins) {
|
|
403
|
+
const merged = {};
|
|
404
|
+
for (const plugin of plugins) {
|
|
405
|
+
const adapters = plugin.tiptapNodeToMarkdown;
|
|
406
|
+
if (!adapters) continue;
|
|
407
|
+
for (const [nodeType, adapter] of Object.entries(adapters)) {
|
|
408
|
+
if (typeof adapter !== "function") {
|
|
409
|
+
console.warn(
|
|
410
|
+
`[ampless markdownAdapters] plugin "${plugin.instanceId ?? plugin.name}" declares a non-function tiptapNodeToMarkdown entry for nodeType "${nodeType}". Skipping the entry.`
|
|
411
|
+
);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const existing = merged[nodeType];
|
|
415
|
+
if (existing && existing !== adapter) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`[ampless markdownAdapters] duplicate tiptap nodeType "${nodeType}" \u2014 already registered by another plugin. Each nodeType may be claimed by at most one plugin.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
merged[nodeType] = adapter;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return merged;
|
|
424
|
+
}
|
|
402
425
|
function htmlPassthrough(html) {
|
|
403
426
|
return createElement("span", { dangerouslySetInnerHTML: { __html: html } });
|
|
404
427
|
}
|
|
@@ -776,9 +799,19 @@ function tiptapNodeToMarkdown(node, opts) {
|
|
|
776
799
|
`;
|
|
777
800
|
}
|
|
778
801
|
default:
|
|
779
|
-
return children;
|
|
802
|
+
if (children) return children;
|
|
803
|
+
console.warn(
|
|
804
|
+
`[ampless] tiptapToMarkdown: no adapter for node type "${String(node.type)}" \u2014 emitting placeholder`
|
|
805
|
+
);
|
|
806
|
+
return `
|
|
807
|
+
<!-- ampless:unsupported-node type="${sanitizeNodeTypeForComment(node.type)}" -->
|
|
808
|
+
|
|
809
|
+
`;
|
|
780
810
|
}
|
|
781
811
|
}
|
|
812
|
+
function sanitizeNodeTypeForComment(type) {
|
|
813
|
+
return String(type).replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
814
|
+
}
|
|
782
815
|
function tiptapTableToMarkdown(node, opts) {
|
|
783
816
|
const rows = node.content ?? [];
|
|
784
817
|
if (rows.length === 0) return "";
|
|
@@ -902,6 +935,62 @@ function convertHtmlTaskList(items) {
|
|
|
902
935
|
}
|
|
903
936
|
return out.join("\n");
|
|
904
937
|
}
|
|
938
|
+
function postToMarkdown(post, opts = {}) {
|
|
939
|
+
const frontmatter = opts.frontmatter !== false ? postFrontmatter(post, opts.siteUrl) : "";
|
|
940
|
+
return frontmatter + postBodyToMarkdown(post, opts);
|
|
941
|
+
}
|
|
942
|
+
function postFrontmatter(post, siteUrl) {
|
|
943
|
+
const lines = [];
|
|
944
|
+
lines.push(`title: ${JSON.stringify(post.title)}`);
|
|
945
|
+
lines.push(`slug: ${JSON.stringify(post.slug)}`);
|
|
946
|
+
if (post.publishedAt) lines.push(`publishedAt: ${JSON.stringify(post.publishedAt)}`);
|
|
947
|
+
if (post.updatedAt) lines.push(`updatedAt: ${JSON.stringify(post.updatedAt)}`);
|
|
948
|
+
if (post.tags && post.tags.length > 0) lines.push(`tags: ${JSON.stringify(post.tags)}`);
|
|
949
|
+
if (post.excerpt) lines.push(`excerpt: ${JSON.stringify(post.excerpt)}`);
|
|
950
|
+
if (siteUrl) {
|
|
951
|
+
const origin = siteUrl.replace(/\/+$/, "");
|
|
952
|
+
lines.push(`canonical: ${JSON.stringify(`${origin}/${post.slug}`)}`);
|
|
953
|
+
}
|
|
954
|
+
return `---
|
|
955
|
+
${lines.join("\n")}
|
|
956
|
+
---
|
|
957
|
+
|
|
958
|
+
`;
|
|
959
|
+
}
|
|
960
|
+
function postBodyToMarkdown(post, opts) {
|
|
961
|
+
switch (post.format) {
|
|
962
|
+
case "markdown":
|
|
963
|
+
return typeof post.body === "string" ? post.body : String(post.body ?? "");
|
|
964
|
+
case "tiptap":
|
|
965
|
+
return tiptapToMarkdown(post.body, { nodeAdapters: opts.nodeAdapters });
|
|
966
|
+
case "html":
|
|
967
|
+
return htmlToMarkdown(String(post.body));
|
|
968
|
+
case "static": {
|
|
969
|
+
const link = `[${staticEntrypoint(post.body)}](/${post.slug}/)`;
|
|
970
|
+
return post.excerpt ? `${link}
|
|
971
|
+
|
|
972
|
+
${post.excerpt}
|
|
973
|
+
` : `${link}
|
|
974
|
+
`;
|
|
975
|
+
}
|
|
976
|
+
default:
|
|
977
|
+
return "";
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function staticEntrypoint(body) {
|
|
981
|
+
let manifest = body;
|
|
982
|
+
if (typeof manifest === "string") {
|
|
983
|
+
try {
|
|
984
|
+
manifest = JSON.parse(manifest);
|
|
985
|
+
} catch {
|
|
986
|
+
return "index.html";
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (typeof manifest === "object" && manifest !== null && typeof manifest.entrypoint === "string" && manifest.entrypoint.length > 0) {
|
|
990
|
+
return manifest.entrypoint;
|
|
991
|
+
}
|
|
992
|
+
return "index.html";
|
|
993
|
+
}
|
|
905
994
|
|
|
906
995
|
// src/plugin-package-manifest.ts
|
|
907
996
|
var TRUST_LEVELS = ["untrusted", "trusted", "privileged"];
|
|
@@ -1520,6 +1609,7 @@ function createPluginHead(cmsConfig, pluginSettings) {
|
|
|
1520
1609
|
}
|
|
1521
1610
|
}
|
|
1522
1611
|
const contentFieldsRegistry = buildContentFieldRegistry(validPlugins);
|
|
1612
|
+
const markdownAdapters = buildMarkdownAdapterRegistry(validPlugins);
|
|
1523
1613
|
for (const plugin of validPlugins) {
|
|
1524
1614
|
const caps = plugin.capabilities;
|
|
1525
1615
|
if (!caps) continue;
|
|
@@ -1581,6 +1671,7 @@ function createPluginHead(cmsConfig, pluginSettings) {
|
|
|
1581
1671
|
return collectPostScriptsForPage(validPlugins, cmsConfig.site, snapshot, posts);
|
|
1582
1672
|
},
|
|
1583
1673
|
contentFieldsRegistry,
|
|
1674
|
+
markdownAdapters,
|
|
1584
1675
|
async contextForPlugins() {
|
|
1585
1676
|
const snapshot = await pluginSettings.loadAll();
|
|
1586
1677
|
return (plugin) => makeCtx(plugin, cmsConfig.site, snapshot);
|
|
@@ -1847,6 +1938,19 @@ function createAmpless(opts) {
|
|
|
1847
1938
|
});
|
|
1848
1939
|
},
|
|
1849
1940
|
renderBodyHtmlString: (post) => renderBodyHtmlString(post),
|
|
1941
|
+
postToMarkdown: async (post, o) => {
|
|
1942
|
+
if (o?.frontmatter === false) {
|
|
1943
|
+
return postToMarkdown(post, {
|
|
1944
|
+
nodeAdapters: pluginHead.markdownAdapters,
|
|
1945
|
+
frontmatter: false
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
const { site } = await settings.loadSiteSettings();
|
|
1949
|
+
return postToMarkdown(post, {
|
|
1950
|
+
nodeAdapters: pluginHead.markdownAdapters,
|
|
1951
|
+
siteUrl: site.url || void 0
|
|
1952
|
+
});
|
|
1953
|
+
},
|
|
1850
1954
|
renderThemeCss: (cssVars) => renderThemeCss(cssVars),
|
|
1851
1955
|
publicAssetUrl: (key) => storage.publicAssetUrl(key),
|
|
1852
1956
|
isStorageConfigured: () => storage.isStorageConfigured(),
|
|
@@ -1870,6 +1974,7 @@ export {
|
|
|
1870
1974
|
SUPPORTED_API_VERSION,
|
|
1871
1975
|
_resetStreamS3Cache,
|
|
1872
1976
|
buildContentFieldRegistry,
|
|
1977
|
+
buildMarkdownAdapterRegistry,
|
|
1873
1978
|
createAmpless,
|
|
1874
1979
|
createPluginHead,
|
|
1875
1980
|
createPluginSettings,
|
|
@@ -1877,6 +1982,7 @@ export {
|
|
|
1877
1982
|
htmlToMarkdown,
|
|
1878
1983
|
loadPackageManifest,
|
|
1879
1984
|
markdownToHtml,
|
|
1985
|
+
postToMarkdown,
|
|
1880
1986
|
renderBody,
|
|
1881
1987
|
renderBodyHtmlString,
|
|
1882
1988
|
renderThemeCss,
|
package/dist/middleware.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ declare function computeCacheControl(flags: {
|
|
|
32
32
|
* - Rewrite to `/static/<slug>(/<path>)` when the post is a static
|
|
33
33
|
* bundle (`format='static'`). Themed posts (default) get no
|
|
34
34
|
* rewrite — `app/[slug]/page.tsx` serves them directly.
|
|
35
|
+
* - Rewrite `/<slug>.md` → `/md/<slug>` (any format) unless
|
|
36
|
+
* `cms.config.ai.markdownRoutes` is explicitly `false`.
|
|
35
37
|
* - `Cache-Control` header computed from `post.metadata.cache` +
|
|
36
38
|
* `post.updatedAt` + `cms.config.cache.*` and set on the response.
|
|
37
39
|
* - `?previewTheme` / `?previewColorScheme` → `x-preview-theme` /
|
package/dist/middleware.js
CHANGED
|
@@ -135,7 +135,8 @@ var RESERVED_PREFIXES = /* @__PURE__ */ new Set([
|
|
|
135
135
|
"tag",
|
|
136
136
|
// internal rewrite targets — direct hits should not be middleware-driven
|
|
137
137
|
"raw",
|
|
138
|
-
"static"
|
|
138
|
+
"static",
|
|
139
|
+
"md"
|
|
139
140
|
]);
|
|
140
141
|
function createAmplessMiddleware(opts) {
|
|
141
142
|
return async function middleware(request) {
|
|
@@ -160,9 +161,11 @@ function createAmplessMiddleware(opts) {
|
|
|
160
161
|
return passthrough();
|
|
161
162
|
}
|
|
162
163
|
const restPath = segments.slice(1);
|
|
163
|
-
const
|
|
164
|
+
const isMdRequest = restPath.length === 0 && slug.endsWith(".md") && slug.length > 3 && opts.cmsConfig.ai?.markdownRoutes !== false;
|
|
165
|
+
const lookupSlug = isMdRequest ? slug.slice(0, -3) : slug;
|
|
166
|
+
const flags = await getCachedFlags(opts, lookupSlug);
|
|
164
167
|
if (!flags) {
|
|
165
|
-
if (restPath.length > 0) {
|
|
168
|
+
if (restPath.length > 0 || isMdRequest) {
|
|
166
169
|
return new NextResponse("Not Found", { status: 404 });
|
|
167
170
|
}
|
|
168
171
|
return passthrough();
|
|
@@ -170,7 +173,12 @@ function createAmplessMiddleware(opts) {
|
|
|
170
173
|
const cacheControl = computeCacheControl(flags, opts.cmsConfig);
|
|
171
174
|
let response;
|
|
172
175
|
if (restPath.length === 0) {
|
|
173
|
-
if (
|
|
176
|
+
if (isMdRequest) {
|
|
177
|
+
url.pathname = `/md/${lookupSlug}`;
|
|
178
|
+
response = NextResponse.rewrite(url, {
|
|
179
|
+
request: { headers: requestHeaders }
|
|
180
|
+
});
|
|
181
|
+
} else if (flags.format === "html" && flags.metadata?.no_layout === true) {
|
|
174
182
|
url.pathname = `/raw/${slug}`;
|
|
175
183
|
response = NextResponse.rewrite(url, {
|
|
176
184
|
request: { headers: requestHeaders }
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -5,18 +5,18 @@ import 'react';
|
|
|
5
5
|
import 'aws-amplify/storage/server';
|
|
6
6
|
import 'next/headers';
|
|
7
7
|
|
|
8
|
-
interface Ctx$
|
|
8
|
+
interface Ctx$5 {
|
|
9
9
|
params: Promise<{
|
|
10
10
|
slug: string;
|
|
11
11
|
}>;
|
|
12
12
|
}
|
|
13
|
-
type OgRouteHandler = (req: Request, ctx: Ctx$
|
|
13
|
+
type OgRouteHandler = (req: Request, ctx: Ctx$5) => Promise<Response>;
|
|
14
14
|
declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
|
|
15
15
|
|
|
16
|
-
interface Ctx$
|
|
16
|
+
interface Ctx$4 {
|
|
17
17
|
params: Promise<Record<string, never>>;
|
|
18
18
|
}
|
|
19
|
-
type SitemapRouteHandler = (req: Request, ctx: Ctx$
|
|
19
|
+
type SitemapRouteHandler = (req: Request, ctx: Ctx$4) => Promise<Response>;
|
|
20
20
|
/**
|
|
21
21
|
* Sitemap route delegate. Looks up the active theme and forwards to
|
|
22
22
|
* whichever `routes.sitemap` handler the theme provides. Themes
|
|
@@ -24,10 +24,10 @@ type SitemapRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
|
|
|
24
24
|
*/
|
|
25
25
|
declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
|
|
26
26
|
|
|
27
|
-
interface Ctx$
|
|
27
|
+
interface Ctx$3 {
|
|
28
28
|
params: Promise<Record<string, never>>;
|
|
29
29
|
}
|
|
30
|
-
type FeedRouteHandler = (req: Request, ctx: Ctx$
|
|
30
|
+
type FeedRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
|
|
31
31
|
/**
|
|
32
32
|
* Feed route delegate. Looks up the active theme and forwards to
|
|
33
33
|
* whichever `routes.feed` handler the theme provides. Themes without
|
|
@@ -35,12 +35,12 @@ type FeedRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
|
35
35
|
*/
|
|
36
36
|
declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
|
|
37
37
|
|
|
38
|
-
interface Ctx$
|
|
38
|
+
interface Ctx$2 {
|
|
39
39
|
params: Promise<{
|
|
40
40
|
slug: string;
|
|
41
41
|
}>;
|
|
42
42
|
}
|
|
43
|
-
type RawRouteHandler = (req: Request, ctx: Ctx$
|
|
43
|
+
type RawRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
44
44
|
/**
|
|
45
45
|
* Internal route handler for `format: 'html'` posts whose
|
|
46
46
|
* `metadata.no_layout === true` — the body is its own complete HTML
|
|
@@ -63,13 +63,13 @@ type RawRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
|
63
63
|
*/
|
|
64
64
|
declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
|
|
65
65
|
|
|
66
|
-
interface Ctx {
|
|
66
|
+
interface Ctx$1 {
|
|
67
67
|
params: Promise<{
|
|
68
68
|
slug: string;
|
|
69
69
|
path?: string[];
|
|
70
70
|
}>;
|
|
71
71
|
}
|
|
72
|
-
type StaticRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
72
|
+
type StaticRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
73
73
|
/**
|
|
74
74
|
* Internal route handler for `format: 'static'` posts — the body is a
|
|
75
75
|
* manifest describing a bundle of files in S3 at
|
|
@@ -97,4 +97,26 @@ type StaticRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
|
97
97
|
*/
|
|
98
98
|
declare function createStaticRouteHandler(ampless: Ampless): StaticRouteHandler;
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
interface Ctx {
|
|
101
|
+
params: Promise<{
|
|
102
|
+
slug: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
type MarkdownRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
106
|
+
/**
|
|
107
|
+
* Route handler serving the per-post Markdown projection
|
|
108
|
+
* (`ampless.postToMarkdown()`) for published posts of any format.
|
|
109
|
+
*
|
|
110
|
+
* Mounted at `app/md/[slug]/route.ts`; reached via middleware rewrite
|
|
111
|
+
* of `/<slug>.md` → `/md/<slug>` internally — the public/canonical URL
|
|
112
|
+
* is `/<slug>.md`. Middleware adds `md` to its reserved-prefix list so
|
|
113
|
+
* a user post with slug `md` passes through to a 404 rather than
|
|
114
|
+
* reaching this handler with the wrong content.
|
|
115
|
+
*
|
|
116
|
+
* Cache-Control: deliberately omitted from the response. Middleware
|
|
117
|
+
* computes the strategy from `post.metadata.cache` + `post.updatedAt`
|
|
118
|
+
* and sets the header on the rewritten response.
|
|
119
|
+
*/
|
|
120
|
+
declare function createMarkdownRouteHandler(ampless: Ampless): MarkdownRouteHandler;
|
|
121
|
+
|
|
122
|
+
export { type FeedRouteHandler, type MarkdownRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createMarkdownRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
|
package/dist/routes/index.js
CHANGED
|
@@ -184,8 +184,31 @@ async function signStaticAsset({
|
|
|
184
184
|
return null;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
// src/routes/md.ts
|
|
189
|
+
function createMarkdownRouteHandler(ampless) {
|
|
190
|
+
return async function GET(_request, { params }) {
|
|
191
|
+
const { slug } = await params;
|
|
192
|
+
const cleanSlug = slug.replace(/\.md$/, "");
|
|
193
|
+
if (ampless.cmsConfig.ai?.markdownRoutes === false) {
|
|
194
|
+
return new Response("Not Found", { status: 404 });
|
|
195
|
+
}
|
|
196
|
+
const post = await ampless.getPublishedPost(cleanSlug);
|
|
197
|
+
if (!post) {
|
|
198
|
+
return new Response("Not Found", { status: 404 });
|
|
199
|
+
}
|
|
200
|
+
return new Response(await ampless.postToMarkdown(post), {
|
|
201
|
+
status: 200,
|
|
202
|
+
headers: {
|
|
203
|
+
"Content-Type": "text/markdown; charset=utf-8"
|
|
204
|
+
// Cache-Control set by middleware.
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
}
|
|
187
209
|
export {
|
|
188
210
|
createFeedRouteHandler,
|
|
211
|
+
createMarkdownRouteHandler,
|
|
189
212
|
createOgRouteHandler,
|
|
190
213
|
createRawRouteHandler,
|
|
191
214
|
createSitemapRouteHandler,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.69",
|
|
4
4
|
"description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"marked": "^18.0.4",
|
|
52
52
|
"sanitize-html": "^2.17.4",
|
|
53
53
|
"tailwind-merge": "^3.6.0",
|
|
54
|
-
"@ampless/plugin-og-image": "0.2.0-beta.
|
|
55
|
-
"ampless": "1.0.0-beta.
|
|
54
|
+
"@ampless/plugin-og-image": "0.2.0-beta.56",
|
|
55
|
+
"ampless": "1.0.0-beta.56"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@aws-amplify/adapter-nextjs": "^1",
|