@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 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, TiptapNodeMarkdownAdapters, Config, ThemeModule, ThemeManifest, PluginPackageManifest } from 'ampless';
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,
@@ -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` /
@@ -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 flags = await getCachedFlags(opts, slug);
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 (flags.format === "html" && flags.metadata?.no_layout === true) {
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 }
@@ -5,18 +5,18 @@ import 'react';
5
5
  import 'aws-amplify/storage/server';
6
6
  import 'next/headers';
7
7
 
8
- interface Ctx$4 {
8
+ interface Ctx$5 {
9
9
  params: Promise<{
10
10
  slug: string;
11
11
  }>;
12
12
  }
13
- type OgRouteHandler = (req: Request, ctx: Ctx$4) => Promise<Response>;
13
+ type OgRouteHandler = (req: Request, ctx: Ctx$5) => Promise<Response>;
14
14
  declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
15
15
 
16
- interface Ctx$3 {
16
+ interface Ctx$4 {
17
17
  params: Promise<Record<string, never>>;
18
18
  }
19
- type SitemapRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
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$2 {
27
+ interface Ctx$3 {
28
28
  params: Promise<Record<string, never>>;
29
29
  }
30
- type FeedRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
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$1 {
38
+ interface Ctx$2 {
39
39
  params: Promise<{
40
40
  slug: string;
41
41
  }>;
42
42
  }
43
- type RawRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
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
- export { type FeedRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
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 };
@@ -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.67",
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",
55
- "ampless": "1.0.0-beta.55"
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",