Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deploy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions deploy/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
13 changes: 9 additions & 4 deletions scripts/build-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,13 @@ function nav(current) {
`<div class="mrow foot">${building}Nothing is out yet</div></nav></div></header>`;
}

// 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) => `<span class="appi${sm ? " sm" : ""} ${a.cls}" aria-hidden="true">${ic(a.icon)}</span>`;

function appCard(a, { withPreview = true } = {}) {
return `<a class="acard" href="/${a.key}/" data-group="${a.group}">${withPreview ? frame(PREVIEWS[a.key].card, { cls: "cardpv" }) : ""}` +
return `<a class="acard" href="${appPath(a.key)}" data-group="${a.group}">${withPreview ? frame(PREVIEWS[a.key].card, { cls: "cardpv" }) : ""}` +
`<div class="head"><h2>${appIcon(a)}${a.name}</h2><span class="end">${building}</span></div><p>${a.line}</p>` +
`<div class="plat">${ic("i-laptop")}${PLATFORMS[a.key]}</div></a>`;
}
Expand Down Expand Up @@ -329,7 +332,7 @@ ${SEC_ROWS.map(([k, s, a]) => `<div class="sec-row" role="row"><b role="rowheade

for (const a of APPS) {
const rest = APPS.filter((x) => x !== a);
pages.push({ file: `${a.key}/index.html`, path: `/${a.key}/`, title: `${a.name} · Orbyn`, current: "Apps",
pages.push({ file: `${a.key}/index.html`, path: appPath(a.key), title: `${a.name} · Orbyn`, current: "Apps",
description: `${a.line} Being built. There is nothing to install yet.`,
body: `<section class="pvhdr"><div class="txt">
<span class="meta">${appIcon(a, false)}${building}<span class="t3 small">${a.group}</span></span>
Expand All @@ -356,7 +359,7 @@ ${PREVIEWS[a.key].phoneLast ? fig(PREVIEWS[a.key].phoneLast[0], b("Phone", PREVI
<div class="gap28"></div>
${promises()}
<h2 class="family t3">The rest of the family</h2>
<div class="cards c3">${rest.map((r) => `<a class="acard slim" href="/${r.key}/"><div class="head"><h2>${appIcon(r)}${r.name}</h2><span class="end">${building}</span></div></a>`).join("")}</div>` });
<div class="cards c3">${rest.map((r) => `<a class="acard slim" href="${appPath(r.key)}"><div class="head"><h2>${appIcon(r)}${r.name}</h2><span class="end">${building}</span></div></a>`).join("")}</div>` });
}

const notFound = (line, button, path, file) => ({ file, path, title: "Not found · Orbyn", current: "",
Expand Down Expand Up @@ -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");
Expand Down
3 changes: 2 additions & 1 deletion scripts/serve-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 2 additions & 0 deletions site/public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Cloudflare Workers static assets. nginx.conf has the same rule.
/planner/ /planner 301
Loading