mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
fix(docs-site): stop doubling the /ktx basePath on alias-host redirects (#263)
ktx.sh/ and docs.ktx.sh/ redirected to https://docs.kaelio.com/ktx/ktx/docs/... (note the doubled /ktx) and 404'd. The host-agnostic `source: "/"` redirect ran before the alias-host canonicalizers, so it injected the /ktx basePath into the path on the alias domains, which the alias catch-all then prepended a second time. Reorder redirects() so alias-host canonicalization runs first, leaving the generic root/docs rules for the local/canonical host only. The /stars exclusion stays because redirects run before beforeFiles rewrites. Add Host-spoofing regression tests (the prior tests only used localhost, which never exercised the alias-host rules) and remove the vestigial website/vercel.json, which the live ktx.sh routing already bypasses. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d14227468b
commit
d3e20df1d5
3 changed files with 110 additions and 32 deletions
|
|
@ -30,7 +30,36 @@ const config = {
|
|||
};
|
||||
},
|
||||
async redirects() {
|
||||
// Alias-host canonicalization MUST come before the generic root/docs
|
||||
// redirects below. Those generic rules have no host guard, so if they ran
|
||||
// first they would inject a "/ktx" basePath into the path on the alias
|
||||
// hosts, which the alias catch-alls would then prepend a second time —
|
||||
// producing https://docs.kaelio.com/ktx/ktx/docs/... Redirects also run
|
||||
// before beforeFiles rewrites, so the ktx.sh catch-all must exclude
|
||||
// /stars* to let the stars dashboard rewrite proxy through.
|
||||
return [
|
||||
{
|
||||
source: "/slack",
|
||||
has: [{ type: "host", value: "ktx.sh" }],
|
||||
destination:
|
||||
"https://join.slack.com/t/ktxcommunity/shared_invite/zt-3y9b44m1x-LVyNNJD5nwaZHq4XS29LMQ",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/:path*",
|
||||
has: [{ type: "host", value: "docs.ktx.sh" }],
|
||||
destination: "https://docs.kaelio.com/ktx/:path*",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/:path((?!stars(?:/|$)).*)",
|
||||
has: [{ type: "host", value: "ktx.sh" }],
|
||||
destination: "https://docs.kaelio.com/ktx/:path",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/",
|
||||
destination: "/ktx/docs/getting-started/introduction",
|
||||
|
|
@ -43,28 +72,6 @@ const config = {
|
|||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/:path*",
|
||||
has: [{ type: "host", value: "docs.ktx.sh" }],
|
||||
destination: "https://docs.kaelio.com/ktx/:path*",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/slack",
|
||||
has: [{ type: "host", value: "ktx.sh" }],
|
||||
destination:
|
||||
"https://join.slack.com/t/ktxcommunity/shared_invite/zt-3y9b44m1x-LVyNNJD5nwaZHq4XS29LMQ",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/:path((?!stars(?:/|$)).*)",
|
||||
has: [{ type: "host", value: "ktx.sh" }],
|
||||
destination: "https://docs.kaelio.com/ktx/:path",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import assert from "node:assert/strict";
|
|||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createServer } from "node:net";
|
||||
import { after, before, test } from "node:test";
|
||||
|
|
@ -100,6 +102,37 @@ after(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
// Node's fetch (undici) overwrites the Host header with the connection host,
|
||||
// so the alias-host redirect rules never match. The low-level http(s) client
|
||||
// sends Host verbatim, which is what the alias canonicalization keys off of.
|
||||
function requestWithHost(hostHeader, path) {
|
||||
const target = new URL(docsSiteUrl);
|
||||
const client = target.protocol === "https:" ? https : http;
|
||||
const port =
|
||||
target.port || (target.protocol === "https:" ? "443" : "80");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = client.request(
|
||||
{
|
||||
hostname: target.hostname,
|
||||
port,
|
||||
path,
|
||||
method: "GET",
|
||||
headers: { Host: hostHeader },
|
||||
},
|
||||
(response) => {
|
||||
response.resume();
|
||||
resolve({
|
||||
status: response.statusCode,
|
||||
location: response.headers.location,
|
||||
});
|
||||
},
|
||||
);
|
||||
request.on("error", reject);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
test("/ktx/docs redirects to the docs introduction", async () => {
|
||||
const response = await fetch(`${docsSiteUrl}${docsBasePath}/docs`, {
|
||||
redirect: "manual",
|
||||
|
|
@ -141,3 +174,51 @@ test("/ktx/api/search returns docs search results", async () => {
|
|||
"search should return at least one docs result",
|
||||
);
|
||||
});
|
||||
|
||||
test("ktx.sh canonicalizes to a single /ktx basePath on the docs host", async () => {
|
||||
const root = await requestWithHost("ktx.sh", "/");
|
||||
assert.equal(root.status, 308);
|
||||
assert.equal(root.location, "https://docs.kaelio.com/ktx/");
|
||||
assert.ok(
|
||||
!root.location.includes("/ktx/ktx"),
|
||||
"the basePath must not be doubled",
|
||||
);
|
||||
|
||||
const page = await requestWithHost(
|
||||
"ktx.sh",
|
||||
"/docs/getting-started/quickstart",
|
||||
);
|
||||
assert.equal(page.status, 308);
|
||||
assert.equal(
|
||||
page.location,
|
||||
"https://docs.kaelio.com/ktx/docs/getting-started/quickstart",
|
||||
);
|
||||
});
|
||||
|
||||
test("docs.ktx.sh canonicalizes to a single /ktx basePath on the docs host", async () => {
|
||||
const root = await requestWithHost("docs.ktx.sh", "/");
|
||||
assert.equal(root.status, 308);
|
||||
assert.equal(root.location, "https://docs.kaelio.com/ktx");
|
||||
assert.ok(
|
||||
!root.location.includes("/ktx/ktx"),
|
||||
"the basePath must not be doubled",
|
||||
);
|
||||
|
||||
const page = await requestWithHost("docs.ktx.sh", "/llms.txt");
|
||||
assert.equal(page.status, 308);
|
||||
assert.equal(page.location, "https://docs.kaelio.com/ktx/llms.txt");
|
||||
});
|
||||
|
||||
test("ktx.sh keeps the /slack and /stars exceptions", async () => {
|
||||
const slack = await requestWithHost("ktx.sh", "/slack");
|
||||
assert.equal(slack.status, 307);
|
||||
assert.match(slack.location, /^https:\/\/join\.slack\.com\//);
|
||||
|
||||
// /stars is proxied by a beforeFiles rewrite, so the apex catch-all must not
|
||||
// canonicalize it to the docs host.
|
||||
const stars = await requestWithHost("ktx.sh", "/stars");
|
||||
assert.ok(
|
||||
!(stars.location ?? "").startsWith("https://docs.kaelio.com"),
|
||||
"the stars dashboard must not be redirected to the docs host",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/:path*",
|
||||
"has": [{ "type": "host", "value": "ktx.sh" }],
|
||||
"destination": "https://docs.ktx.sh/:path*",
|
||||
"permanent": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue