@ampless/runtime 0.2.0-alpha.4 → 0.2.0-alpha.5

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.
@@ -25,6 +25,11 @@ function createThemePostDispatcher(ampless) {
25
25
  if (post?.metadata?.no_layout === true) {
26
26
  redirect(`/raw/${slug}`);
27
27
  }
28
+ if (post?.format === "static") {
29
+ const body = post.body ?? null;
30
+ const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
31
+ redirect(`/${slug}/${entrypoint}`);
32
+ }
28
33
  const { module } = await ampless.resolveActiveTheme(siteId);
29
34
  const Post = module.components.Post;
30
35
  if (!Post) notFound();
@@ -2,21 +2,21 @@ import { Ampless } from '../index.js';
2
2
  import 'ampless';
3
3
  import 'next';
4
4
 
5
- interface Ctx$3 {
5
+ interface Ctx$4 {
6
6
  params: Promise<{
7
7
  siteId: string;
8
8
  slug: string;
9
9
  }>;
10
10
  }
11
- type OgRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
11
+ type OgRouteHandler = (req: Request, ctx: Ctx$4) => Promise<Response>;
12
12
  declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
13
13
 
14
- interface Ctx$2 {
14
+ interface Ctx$3 {
15
15
  params: Promise<{
16
16
  siteId: string;
17
17
  }>;
18
18
  }
19
- type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
19
+ type SitemapRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
20
20
  /**
21
21
  * Sitemap route delegate. Looks up the active theme for the request's
22
22
  * siteId and forwards to whichever `routes.sitemap` handler the theme
@@ -24,12 +24,12 @@ type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
24
24
  */
25
25
  declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
26
26
 
27
- interface Ctx$1 {
27
+ interface Ctx$2 {
28
28
  params: Promise<{
29
29
  siteId: string;
30
30
  }>;
31
31
  }
32
- type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
32
+ type FeedRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
33
33
  /**
34
34
  * Feed route delegate. Looks up the active theme for the request's
35
35
  * siteId and forwards to whichever `routes.feed` handler the theme
@@ -37,13 +37,13 @@ type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
37
37
  */
38
38
  declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
39
39
 
40
- interface Ctx {
40
+ interface Ctx$1 {
41
41
  params: Promise<{
42
42
  siteId: string;
43
43
  slug: string;
44
44
  }>;
45
45
  }
46
- type RawRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
46
+ type RawRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
47
47
  /**
48
48
  * Bare HTML route. Returns the published post's rendered body as the
49
49
  * entire HTTP response — no Next.js root layout, no theme chrome.
@@ -77,4 +77,37 @@ type RawRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
77
77
  */
78
78
  declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
79
79
 
80
- export { type FeedRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, createFeedRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler };
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).
93
+ *
94
+ * Routing precedence: Next.js prefers the more specific
95
+ * `[siteId]/[slug]/page.tsx` for single-segment URLs, so this catch-all
96
+ * only fires when path.length >= 2. The post dispatcher detects
97
+ * `format === 'static'` and 308-redirects to `/<slug>/<entrypoint>`
98
+ * so the catch-all picks up from there.
99
+ *
100
+ * Single-segment requests (`/<siteId>/<slug>`) that somehow do reach
101
+ * here are 308-redirected to add a trailing slash, which is what makes
102
+ * relative paths in the bundle resolve correctly in the browser.
103
+ *
104
+ * Implementation: generate a short-lived presigned URL for the S3
105
+ * object and 302-redirect to it. Same model as `/api/media/[...path]`
106
+ * — keeps the bucket private while letting CloudFront cache the 302
107
+ * for the Cache-Control window. The presigned URL itself expires in
108
+ * an hour; the 302 cache is 5 minutes so re-issued presigns stay
109
+ * fresh.
110
+ */
111
+ declare function createStaticRouteHandler(ampless: Ampless): StaticRouteHandler;
112
+
113
+ export { type FeedRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
@@ -87,9 +87,78 @@ function createRawRouteHandler(ampless) {
87
87
  });
88
88
  };
89
89
  }
90
+
91
+ // src/routes/static.ts
92
+ import { createServerRunner } from "@aws-amplify/adapter-nextjs";
93
+ import { cookies } from "next/headers";
94
+ import { getUrl } from "aws-amplify/storage/server";
95
+ var FALLBACK_MIME = {
96
+ ".html": "text/html; charset=utf-8",
97
+ ".htm": "text/html; charset=utf-8"
98
+ };
99
+ function createStaticRouteHandler(ampless) {
100
+ const { runWithAmplifyServerContext } = createServerRunner({
101
+ config: ampless.outputs
102
+ });
103
+ return async function GET(request, { params }) {
104
+ const { siteId, path } = await params;
105
+ if (!path || path.length === 0) {
106
+ return new Response("Not Found", { status: 404 });
107
+ }
108
+ const slug = path[0];
109
+ const restSegments = path.slice(1);
110
+ if (restSegments.some(
111
+ (s) => !s || s === "." || s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0")
112
+ )) {
113
+ return new Response("Invalid path", { status: 400 });
114
+ }
115
+ const post = await ampless.getPublishedPost(slug, { siteId });
116
+ if (!post || post.format !== "static") {
117
+ return new Response("Not Found", { status: 404 });
118
+ }
119
+ const body = post.body ?? null;
120
+ const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
121
+ const fileList = Array.isArray(body?.files) ? body.files : [];
122
+ let rest = restSegments.join("/");
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
+ }
132
+ if (fileList.length > 0 && !fileList.includes(rest)) {
133
+ return new Response("Not Found", { status: 404 });
134
+ }
135
+ const objectPath = `public/static/${siteId}/${slug}/${rest}`;
136
+ try {
137
+ const presignedUrl = await runWithAmplifyServerContext({
138
+ nextServerContext: { cookies },
139
+ operation: async (amplifyContext) => {
140
+ const result = await getUrl(amplifyContext, {
141
+ path: objectPath,
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
+ });
155
+ }
156
+ };
157
+ }
90
158
  export {
91
159
  createFeedRouteHandler,
92
160
  createOgRouteHandler,
93
161
  createRawRouteHandler,
94
- createSitemapRouteHandler
162
+ createSitemapRouteHandler,
163
+ createStaticRouteHandler
95
164
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "0.2.0-alpha.4",
3
+ "version": "0.2.0-alpha.5",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,8 +48,8 @@
48
48
  "clsx": "^2.1.1",
49
49
  "lucide-react": "^1.16.0",
50
50
  "tailwind-merge": "^3.6.0",
51
- "ampless": "0.2.0-alpha.2",
52
- "@ampless/plugin-og-image": "0.2.0-alpha.2"
51
+ "ampless": "0.2.0-alpha.3",
52
+ "@ampless/plugin-og-image": "0.2.0-alpha.3"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "next": "^15 || ^16",