Skip to content

Commit 8267737

Browse files
committed
major fixes with the ux imporovemnts and added cards into gallery with the copywriting improvments and the functionality check
1 parent c0e12e4 commit 8267737

72 files changed

Lines changed: 60059 additions & 638 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/api/example/[...path]/route.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Serves files from the committed `examples/<brand>/` tree so the gallery
2+
// brand page can offer Download buttons (DESIGN.md, tokens.json, tailwind
3+
// .css, shadcn-theme.css) via plain `<a href download>` anchors without
4+
// having to embed the file contents as React props.
5+
//
6+
// Why a separate route from /api/output?
7+
// /api/output TRANSIENT files written by /api/extract at request
8+
// time. Filesystem source is `output/<slug>/`. No-store
9+
// caching because each extraction overwrites the dir.
10+
// /api/example COMMITTED files in the repo. Filesystem source is
11+
// `examples/<slug>/`. Cacheable (the bytes don't
12+
// change between commits) so browser + CDN can keep
13+
// the file warm across page loads.
14+
// Separating the two surfaces means each can have its own caching policy
15+
// and access pattern without one accidentally serving the other's data.
16+
//
17+
// Security: the route must reject anything that escapes the examples
18+
// root. Symlinks, `..`, NUL bytes, and absolute paths are blocked via
19+
// realpath comparison (same defense-in-depth as /api/output).
20+
21+
import * as fs from "fs";
22+
import * as path from "path";
23+
24+
export const runtime = "nodejs";
25+
// `dynamic = 'force-static'` is wrong here because we serve different
26+
// files at different sub-paths; let Next.js cache based on the URL.
27+
// Pages that link to a specific brand's tailwind.css get cached per URL.
28+
29+
const EXAMPLES_ROOT = path.resolve(process.cwd(), "examples");
30+
31+
const CONTENT_TYPES: Record<string, string> = {
32+
".html": "text/html; charset=utf-8",
33+
".json": "application/json; charset=utf-8",
34+
".css": "text/css; charset=utf-8",
35+
".md": "text/markdown; charset=utf-8",
36+
".png": "image/png",
37+
".jpg": "image/jpeg",
38+
".jpeg": "image/jpeg",
39+
".svg": "image/svg+xml",
40+
".txt": "text/plain; charset=utf-8",
41+
};
42+
43+
export async function GET(
44+
_req: Request,
45+
{ params }: { params: Promise<{ path: string[] }> },
46+
) {
47+
const { path: segments } = await params;
48+
49+
// Defense in depth: reject traversal, NUL, and backslash before
50+
// resolving; then verify the resolved path is inside EXAMPLES_ROOT.
51+
for (const seg of segments) {
52+
if (seg.includes("..") || seg.includes("\0") || seg.includes("\\")) {
53+
return new Response("Bad path", { status: 400 });
54+
}
55+
}
56+
57+
const requested = path.resolve(EXAMPLES_ROOT, ...segments);
58+
if (
59+
!requested.startsWith(EXAMPLES_ROOT + path.sep) &&
60+
requested !== EXAMPLES_ROOT
61+
) {
62+
return new Response("Forbidden", { status: 403 });
63+
}
64+
65+
if (!fs.existsSync(requested)) {
66+
return new Response("Not found", { status: 404 });
67+
}
68+
69+
const stat = fs.statSync(requested);
70+
if (!stat.isFile()) {
71+
return new Response("Not a file", { status: 400 });
72+
}
73+
74+
const ext = path.extname(requested).toLowerCase();
75+
const contentType = CONTENT_TYPES[ext] ?? "application/octet-stream";
76+
77+
const buf = fs.readFileSync(requested);
78+
return new Response(new Uint8Array(buf), {
79+
headers: {
80+
"content-type": contentType,
81+
"content-length": String(buf.length),
82+
// Committed files; immutable between deploys. Hour-long browser
83+
// cache + day-long CDN cache. If a brand's files change, the
84+
// deploy rolls a new build hash and clients re-fetch.
85+
"cache-control": "public, max-age=3600, s-maxage=86400",
86+
},
87+
});
88+
}

app/api/extract/route.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { stripDarkScreenshotsOnDisk } from "@/lib/engine/strip-dark-screenshots"
1919
import { applyButtonClustering } from "@/lib/engine/button-cluster";
2020
import { generateAndWriteShadcnCss } from "@/lib/engine/shadcn-emit";
2121
import { checkAndRecordRateLimit, getClientIp } from "@/lib/rate-limit";
22+
import { normalizeUrl } from "@/lib/url-resolver";
2223
import type { ColorToken, TypographyLevel } from "@/lib/engine/types";
2324

2425
// Note on the Turbopack NFT trace warning at build time: these engine
@@ -100,17 +101,6 @@ interface ExtractRequest {
100101
withPhase3?: boolean;
101102
}
102103

103-
function normalizeUrl(raw: string): string | null {
104-
const trimmed = raw.trim();
105-
if (!trimmed) return null;
106-
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
107-
try {
108-
return new URL(withProtocol).toString();
109-
} catch {
110-
return null;
111-
}
112-
}
113-
114104
function slugForOutput(url: string): string {
115105
return new URL(url).hostname.replace(/[^a-z0-9.-]/gi, "-");
116106
}

app/api/output/[...path]/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ export async function GET(
5454
}
5555

5656
if (!fs.existsSync(requested)) {
57+
// Instance-identity probe — pair with the `[extract] wrote tokens.json …`
58+
// line in extract.ts to verify whether the 404 is happening on the SAME
59+
// instance that wrote the files. On Cloud Run, K_REVISION matches the
60+
// revision name; different instance IDs across the two lines = multi-
61+
// instance routing confirmed.
62+
console.log(
63+
`[file-server 404] path=${requested} cwd=${process.cwd()} K_REVISION=${process.env.K_REVISION ?? 'local'}`,
64+
);
5765
return new Response("Not found", { status: 404 });
5866
}
5967

0 commit comments

Comments
 (0)