From 5da9e34a2c6600d397830cbf4b66486eca55c5b7 Mon Sep 17 00:00:00 2001 From: Tran Duc Anh Dang Date: Wed, 11 Nov 2026 12:49:04 +1100 Subject: [PATCH] fix: every page lives at its address with no trailing slash Canonical addresses and internal links are /apps, /pricing, /security, /download, /planner, /notes, /studio and /engineering, as in the designs. The trailing-slash form redirects: a location rule in nginx.conf, and drop-trailing-slash in wrangler.jsonc. The _redirects file from the Planner change is removed: with auto-trailing-slash it made /planner redirect to /planner/ and back forever on Workers. --- deploy/Dockerfile | 4 ++-- deploy/nginx.conf | 4 ++-- scripts/build-site.mjs | 20 ++++++++++---------- scripts/serve-site.mjs | 6 ++---- site/public/_redirects | 2 -- wrangler.jsonc | 2 +- 6 files changed, 17 insertions(+), 21 deletions(-) delete mode 100644 site/public/_redirects diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 8b600be9..059d5d9a 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 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 +# _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 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 e35c790b..6fcc9ee5 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -28,8 +28,8 @@ 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; } + # Every page lives at its address with no trailing slash; the slash form redirects to it. + location ~ ^/(apps|download|pricing|security|planner|notes|studio|engineering)/$ { return 301 /$1; } 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 6d4b3ebc..4e14dc54 100755 --- a/scripts/build-site.mjs +++ b/scripts/build-site.mjs @@ -151,7 +151,7 @@ const PREVIEWS = { const esc = (s) => s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); const building = 'Building'; const planned = 'Planned'; -const NAV = [["Apps", "/apps/"], ["Pricing", "/pricing/"], ["Security", "/security/"]]; +const NAV = [["Apps", "/apps"], ["Pricing", "/pricing"], ["Security", "/security"]]; // Theme: the button and menu from orbyn.dev Designs "Theme switch". const THEMES = [["system", "System", "i-laptop", "Follows this device"], ["light", "Light", "i-sun", ""], ["dark", "Dark", "i-moon", ""], ["dim", "Dim", "i-moon", "Softer dark"]]; @@ -178,9 +178,9 @@ 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}/`); +// Every page lives at its address with no trailing slash (orbyn.dev/planner, orbyn.dev/apps). +// The trailing-slash form redirects there: nginx.conf, and Workers' drop-trailing-slash setting. +const appPath = (key) => `/${key}`; const appIcon = (a, sm = true) => ``; function appCard(a, { withPreview = true } = {}) { @@ -236,7 +236,7 @@ pages.push({ file: "index.html", path: "/", title: "Orbyn", current: "", One account · four apps

Private apps that work offline and that agents can work in safely.

Orbyn is a family of local-first apps that sync end to end under one account. Plan your week, keep your notes, make films and build software.A family of local-first apps under one account.

-
See the appsHow we protect your data
+
See the appsHow we protect your data
Nothing is out yet. Every app is still being built.
@@ -254,7 +254,7 @@ ${APPS.map((a) => `
${appIcon(
${promises()}` }); -pages.push({ file: "apps/index.html", path: "/apps/", title: "Apps · Orbyn", current: "Apps", +pages.push({ file: "apps/index.html", path: "/apps", title: "Apps · Orbyn", current: "Apps", description: "Four apps on one account: Orbyn Planner, Notes, Studio and Engineering. Every app is still being built.", body: `

Apps

@@ -265,7 +265,7 @@ pages.push({ file: "apps/index.html", path: "/apps/", title: "Apps · Orbyn", cu
${APPS.map((a) => appCard(a)).join("")}
${building}Being built, not out yet${planned}On the roadmap, not started
` }); -pages.push({ file: "download/index.html", path: "/download/", title: "Download · Orbyn", current: "", +pages.push({ file: "download/index.html", path: "/download", title: "Download · Orbyn", current: "", description: "Nothing to download yet. Every Orbyn app is still being built.", body: `

Nothing to download yet

@@ -274,7 +274,7 @@ pages.push({ file: "download/index.html", path: "/download/", title: "Download
${APPS.map((a) => `

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

Planned for ${PLATFORMS[a.key]}
${building}
`).join("")}

Phone and web versions are planned for Planner and Notes.

` }); -pages.push({ file: "pricing/index.html", path: "/pricing/", title: "Pricing · Orbyn", current: "Pricing", +pages.push({ file: "pricing/index.html", path: "/pricing", title: "Pricing · Orbyn", current: "Pricing", description: "Nothing is for sale yet. These are the plans Orbyn intends to launch with: every app free on one device, and one subscription to sync.", body: `
${planned} @@ -300,7 +300,7 @@ const SEC_ROWS = [ ["Shared lists and projects", "Standard", "End-to-end when everyone sharing uses Advanced"], ["Calendars other apps use", "Standard", "Standard, so calendar apps and invitations keep working"], ]; -pages.push({ file: "security/index.html", path: "/security/", title: "Security · Orbyn", current: "Security", +pages.push({ file: "security/index.html", path: "/security", title: "Security · Orbyn", current: "Security", description: "How Orbyn protects your data. This is how Orbyn is being built. Nothing has launched yet, and details may change before it does.", body: `
${building}

How Orbyn protects your data

This is how Orbyn is being built. Nothing has launched yet, and details may change before it does.

@@ -365,7 +365,7 @@ ${promises()} const notFound = (line, button, path, file) => ({ file, path, title: "Not found · Orbyn", current: "", description: "This page doesn’t exist.", body: `
404

This page doesn’t exist

${line}

-
` }); +
` }); pages.push(notFound("The link may be wrong, or the page may have moved.", "See the apps", "/404", "404.html")); pages.push(notFound("Booking lives inside Orbyn Planner. It’s planned, not built yet.", "See all apps", "/booking", "booking/404.html")); diff --git a/scripts/serve-site.mjs b/scripts/serve-site.mjs index d56729be..a882492e 100755 --- a/scripts/serve-site.mjs +++ b/scripts/serve-site.mjs @@ -17,10 +17,8 @@ async function file(p) { createServer(async (req, res) => { const path = normalize(decodeURIComponent(new URL(req.url, "http://x").pathname)).replace(/^(\.\.[/\\])+/, ""); - 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(); - } + const slash = path.match(/^(\/(?:apps|download|pricing|security|planner|notes|studio|engineering))\/$/); + if (slash) { res.writeHead(301, { Location: slash[1] }); return res.end(); } let found = path.startsWith("/_headers") ? null : await file(join(dist, path)); let status = 200; if (!found) { diff --git a/site/public/_redirects b/site/public/_redirects deleted file mode 100644 index fc82e1ec..00000000 --- a/site/public/_redirects +++ /dev/null @@ -1,2 +0,0 @@ -# Cloudflare Workers static assets. nginx.conf has the same rule. -/planner/ /planner 301 diff --git a/wrangler.jsonc b/wrangler.jsonc index f033a510..b99133ba 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -8,6 +8,6 @@ "assets": { "directory": "./dist", "not_found_handling": "404-page", - "html_handling": "auto-trailing-slash" + "html_handling": "drop-trailing-slash" } }