diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 059d5d9a..8b600be9 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -4,8 +4,8 @@ ARG GIT_SHA=dev WORKDIR /src COPY scripts/build-site.mjs scripts/ COPY site site -# _headers is for Cloudflare Workers; nginx.conf sends the same headers. -RUN node scripts/build-site.mjs && rm dist/_headers && echo "$GIT_SHA" > dist/version.txt +# _headers and _redirects are for Cloudflare Workers; nginx.conf does the same. +RUN node scripts/build-site.mjs && rm dist/_headers dist/_redirects && echo "$GIT_SHA" > dist/version.txt FROM nginxinc/nginx-unprivileged:1.28-alpine COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 59d0e1a6..e35c790b 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -28,6 +28,9 @@ server { location = /booking/404.html { internal; } location = /404.html { internal; } + # Planner's landing page is exactly /planner; the trailing-slash form redirects to it. + location = /planner/ { return 301 /planner; } + location / { try_files $uri $uri/index.html $uri/ =404; } error_page 404 /404.html; } diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs index 324aa40b..6d4b3ebc 100755 --- a/scripts/build-site.mjs +++ b/scripts/build-site.mjs @@ -178,10 +178,13 @@ function nav(current) { `
${building}Nothing is out yet
`; } +// The Planner landing page lives at exactly https://orbyn.dev/planner (no trailing slash). +// /planner/ redirects there; every link and the canonical address use the exact form. +const appPath = (key) => (key === "planner" ? "/planner" : `/${key}/`); const appIcon = (a, sm = true) => ``; function appCard(a, { withPreview = true } = {}) { - return `${withPreview ? frame(PREVIEWS[a.key].card, { cls: "cardpv" }) : ""}` + + return `${withPreview ? frame(PREVIEWS[a.key].card, { cls: "cardpv" }) : ""}` + `

${appIcon(a)}${a.name}

${building}

${a.line}

` + `
${ic("i-laptop")}${PLATFORMS[a.key]}
`; } @@ -329,7 +332,7 @@ ${SEC_ROWS.map(([k, s, a]) => `
${appIcon(a, false)}${building}${a.group} @@ -356,7 +359,7 @@ ${PREVIEWS[a.key].phoneLast ? fig(PREVIEWS[a.key].phoneLast[0], b("Phone", PREVI
${promises()}

The rest of the family

-
${rest.map((r) => `

${appIcon(r)}${r.name}

${building}
`).join("")}
` }); +
${rest.map((r) => `

${appIcon(r)}${r.name}

${building}
`).join("")}
` }); } const notFound = (line, button, path, file) => ({ file, path, title: "Not found · Orbyn", current: "", @@ -396,8 +399,10 @@ const problems = []; const exists = (href, from) => { const p = href.replace(/[#?].*$/, ""); const base = p.startsWith("/") ? out : dirname(from); + const isFile = (t) => { try { return statSync(t).isFile(); } catch { return false; } }; const target = p.endsWith("/") ? join(base, p, "index.html") : join(base, p); - try { return statSync(target).isFile(); } catch { return false; } + // A path without a trailing slash may name a folder with an index page, as the server serves it. + return isFile(target) || (!p.endsWith("/") && isFile(join(target, "index.html"))); }; for (const f of files.filter((f) => f.endsWith(".html"))) { const html = readFileSync(f, "utf8"); diff --git a/scripts/serve-site.mjs b/scripts/serve-site.mjs index 83efb3d4..d56729be 100755 --- a/scripts/serve-site.mjs +++ b/scripts/serve-site.mjs @@ -17,7 +17,8 @@ async function file(p) { createServer(async (req, res) => { const path = normalize(decodeURIComponent(new URL(req.url, "http://x").pathname)).replace(/^(\.\.[/\\])+/, ""); - if (/^\/(apps|download|pricing|security|planner|notes|studio|engineering)$/.test(path)) { + if (path === "/planner/") { res.writeHead(301, { Location: "/planner" }); return res.end(); } + if (/^\/(apps|download|pricing|security|notes|studio|engineering)$/.test(path)) { res.writeHead(301, { Location: path + "/" }); return res.end(); } let found = path.startsWith("/_headers") ? null : await file(join(dist, path)); diff --git a/site/public/_redirects b/site/public/_redirects new file mode 100644 index 00000000..fc82e1ec --- /dev/null +++ b/site/public/_redirects @@ -0,0 +1,2 @@ +# Cloudflare Workers static assets. nginx.conf has the same rule. +/planner/ /planner 301