diff --git a/package.json b/package.json index 5dd2a33..c29f06e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build": "vocs build", "start": "node server.mjs", "preview": "vocs preview", + "check:redirects": "node scripts/check-redirects.mjs", "format": "prettier --write ." }, "dependencies": { diff --git a/redirects.config.js b/redirects.config.js index 442e4df..09e2c48 100644 --- a/redirects.config.js +++ b/redirects.config.js @@ -125,17 +125,20 @@ export const redirects = [ { from: "/recovery-flow/portal", to: "/advanced/account-recovery/portal" }, // Smart Routing Address (and global-address duplicate) - { from: "/smart-routing-address", to: "/onramp/smart-routing-address" }, - { from: "/global-address", to: "/onramp/smart-routing-address" }, + { from: "/smart-routing-address", to: "/onramp/smart-routing-address/quickstart" }, + { from: "/global-address", to: "/onramp/smart-routing-address/quickstart" }, // Magic Account (legacy) → Chain Abstraction. Was a manual Render redirect to // /sdk/advanced/chain-abstraction, itself now legacy; collapse the hop straight - // to the live page. (/magic-account/* subpaths are handled in render.yaml.) + // to the live page. Every known /magic-account/* subpath has its own rule + // below; the prefix rule in server.mjs catches anything unknown. { from: "/magic-account", to: "/smart-accounts/chain-abstraction/overview" }, // Embedded Wallet docs live at /wallets/* (alpha — unlisted in sidebars). - // React WaaS hooks → Advanced › React Hooks (legacy) + // React WaaS hooks → Advanced › React Hooks. @zerodev/waas is legacy and + // unmaintained, so only these 1:1 page moves point here; anything else that + // merely used waas goes to the current wallet SDK instead. { from: "/react/getting-started", to: "/advanced/react-hooks/getting-started" }, { from: "/react/use-balance", to: "/advanced/react-hooks/use-balance" }, { from: "/react/use-chainid", to: "/advanced/react-hooks/use-chainid" }, @@ -158,13 +161,13 @@ export const redirects = [ { from: "/react/use-wallet-connect", to: "/advanced/react-hooks/use-wallet-connect" }, // Orphan /smart-wallet/* (deleted) → canonical homes in the new tree - { from: "/smart-wallet/which-sdk", to: "/onboarding/create-a-smart-account" }, + { from: "/smart-wallet/which-sdk", to: "/get-started/sdks/overview" }, { from: "/smart-wallet/quickstart-core", to: "/get-started/quickstart" }, - { from: "/smart-wallet/quickstart-react", to: "/get-started/quickstart" }, - { from: "/smart-wallet/quickstart-capabilities", to: "/get-started/quickstart" }, - { from: "/smart-wallet/creating-wallets", to: "/onboarding/create-a-smart-account" }, - { from: "/smart-wallet/setting-up-zerodev-projects", to: "/" }, - { from: "/smart-wallet/sending-transactions", to: "/smart-accounts/send-transactions" }, + { from: "/smart-wallet/quickstart-react", to: "/wallets/quickstart" }, + { from: "/smart-wallet/quickstart-capabilities", to: "/wallets/quickstart" }, + { from: "/smart-wallet/creating-wallets", to: "/wallets/quickstart" }, + { from: "/smart-wallet/setting-up-zerodev-projects", to: "/get-started/sdks/setup-project" }, + { from: "/smart-wallet/sending-transactions", to: "/wallets/wallet-api/send-transaction" }, { from: "/smart-wallet/batching-transactions", to: "/smart-accounts/batch-transactions" }, { from: "/smart-wallet/pay-gas-in-erc20s", to: "/smart-accounts/pay-gas-with-erc20s" }, { from: "/smart-wallet/sponsoring-gas", to: "/smart-accounts/sponsor-gas/evm" }, @@ -223,7 +226,7 @@ export const redirects = [ { from: "/smart-accounts/account-recovery/flow-setup", to: "/advanced/account-recovery/flow-setup" }, { from: "/smart-accounts/account-recovery/portal", to: "/advanced/account-recovery/portal" }, { from: "/smart-accounts/eip-7702/quickstart", to: "/get-started/eip-7702/quickstart" }, - { from: "/cross-chain/smart-routing-address", to: "/onramp/smart-routing-address" }, + { from: "/cross-chain/smart-routing-address", to: "/onramp/smart-routing-address/quickstart" }, { from: "/cross-chain/chain-abstraction/overview", to: "/smart-accounts/chain-abstraction/overview" }, { from: "/cross-chain/chain-abstraction/supported-base-tokens", to: "/smart-accounts/chain-abstraction/supported-base-tokens" }, { from: "/cross-chain/chain-abstraction/supported-defi-tokens", to: "/smart-accounts/chain-abstraction/supported-defi-tokens" }, @@ -281,4 +284,159 @@ export const redirects = [ { from: "/blog/zerodev-acquired", to: "https://www.zerodev.app/blogs/blog-zerodev-acquired" }, { from: "/blog/zerodev-glider", to: "https://www.zerodev.app/blogs/blog-zerodev-glider" }, { from: "/blog/zerodev-litprotocol", to: "https://www.zerodev.app/blogs/blog-zerodev-litprotocol" }, + + // 2026-06 IA revamp fallout. Sources found with + // `git log --first-parent --full-history origin/main`, never `--all`: a page + // that only ever lived on a feature branch was never deployed, and a rule for + // one would 301 those pages away if the branch later merged. + // + // /modules/offramp and /smart-wallet/offramp are absent on purpose. Both were + // one-line stubs with no successor, so they 404. + + // Legacy /sdk/* + { from: "/sdk/advanced/magic-address", to: "/onramp/smart-routing-address/quickstart" }, + { from: "/sdk/core-api/intro", to: "/onboarding/create-a-smart-account" }, + { from: "/sdk/core-api/pay-gas-in-erc20s", to: "/smart-accounts/pay-gas-with-erc20s" }, + { from: "/sdk/core-api/sign-and-validate-messages", to: "/smart-accounts/sign-and-verify" }, + { from: "/sdk/core-api/wallet-connect", to: "/advanced/wallet-connect" }, + { from: "/sdk/faqs/why-chain-abstraction", to: "/smart-accounts/chain-abstraction/overview" }, + { from: "/sdk/getting-started/wallet-connect", to: "/advanced/wallet-connect" }, + { from: "/sdk/infra/stackup", to: "/api-and-toolings/infrastructure/choose-an-infra-provider" }, + { from: "/sdk/intro", to: "/" }, + { from: "/sdk/permissions/getting-started", to: "/smart-accounts/permissions/session-keys" }, + { from: "/sdk/plugins/ecdsa", to: "/smart-accounts/permissions/signers/ecdsa" }, + { from: "/sdk/plugins/guardians", to: "/advanced/account-recovery/sdk-recovery" }, + { from: "/sdk/plugins/intro", to: "/smart-accounts/use-plugins/overview" }, + { from: "/sdk/plugins/multisig", to: "/advanced/multisig" }, + { from: "/sdk/plugins/passkey", to: "/onboarding/passkeys/overview" }, + { from: "/sdk/plugins/passkeys", to: "/onboarding/passkeys/overview" }, + { from: "/sdk/plugins/recovery", to: "/advanced/account-recovery/sdk-recovery" }, + { from: "/sdk/plugins/session-keys", to: "/smart-accounts/permissions/session-keys" }, + { from: "/sdk/plugins/weighted-ecdsa", to: "/advanced/multisig" }, + + // kerneljs.com docs, merged into this site + { from: "/kerneljs/core-api/batch-transactions", to: "/smart-accounts/batch-transactions" }, + { from: "/kerneljs/core-api/create-account", to: "/onboarding/create-a-smart-account" }, + { from: "/kerneljs/core-api/delegatecall", to: "/smart-accounts/delegatecall" }, + { from: "/kerneljs/core-api/deploy-contract", to: "/smart-accounts/deploy-contract" }, + { from: "/kerneljs/core-api/intro", to: "/onboarding/create-a-smart-account" }, + { from: "/kerneljs/core-api/pay-gas-in-erc20s", to: "/smart-accounts/pay-gas-with-erc20s" }, + { from: "/kerneljs/core-api/send-transactions", to: "/smart-accounts/send-transactions" }, + { from: "/kerneljs/core-api/sign-and-validate-messages", to: "/smart-accounts/sign-and-verify" }, + { from: "/kerneljs/core-api/sponsor-gas", to: "/smart-accounts/sponsor-gas/evm" }, + { from: "/kerneljs/getting-started/intro", to: "/" }, + { from: "/kerneljs/getting-started/quickstart", to: "/get-started/quickstart" }, + { from: "/kerneljs/plugins/ecdsa", to: "/smart-accounts/permissions/signers/ecdsa" }, + { from: "/kerneljs/plugins/intro", to: "/smart-accounts/use-plugins/overview" }, + { from: "/kerneljs/plugins/multisig", to: "/advanced/multisig" }, + { from: "/kerneljs/plugins/passkey", to: "/onboarding/passkeys/overview" }, + { from: "/kerneljs/plugins/recovery", to: "/advanced/account-recovery/sdk-recovery" }, + { from: "/kerneljs/plugins/session-keys", to: "/smart-accounts/permissions/session-keys" }, + { from: "/kerneljs/presets/intro", to: "/api-and-toolings/presets/intro" }, + { from: "/kerneljs/presets/zerodev", to: "/api-and-toolings/presets/zerodev" }, + { from: "/kerneljs/signers/arcana", to: "/onboarding/arcana" }, + { from: "/kerneljs/signers/capsule", to: "/onboarding/capsule" }, + { from: "/kerneljs/signers/custom-signer", to: "/onboarding/custom-signer" }, + { from: "/kerneljs/signers/dfns", to: "/onboarding/dfns" }, + { from: "/kerneljs/signers/dynamic", to: "/onboarding/dynamic" }, + { from: "/kerneljs/signers/eoa", to: "/onboarding/eoa" }, + { from: "/kerneljs/signers/fireblocks", to: "/onboarding/fireblocks" }, + { from: "/kerneljs/signers/intro", to: "/onboarding/auth-providers" }, + { from: "/kerneljs/signers/lit-protocol", to: "/onboarding/lit-protocol" }, + { from: "/kerneljs/signers/magic", to: "/onboarding/magic" }, + { from: "/kerneljs/signers/particle", to: "/onboarding/particle" }, + { from: "/kerneljs/signers/portal", to: "/onboarding/portal" }, + { from: "/kerneljs/signers/privy", to: "/onboarding/privy" }, + { from: "/kerneljs/signers/turnkey", to: "/onboarding/turnkey" }, + { from: "/kerneljs/signers/web3auth", to: "/onboarding/web3auth" }, + + // /wallets/* moved or renamed + { from: "/wallets/auth/wallet-ui-kit", to: "/wallets/auth/wallet-ui-kit/getting-started" }, + { from: "/wallets/hooks/use-authenticate-oauth", to: "/wallets/auth/google-oauth" }, + { from: "/wallets/hooks/use-export-private-key", to: "/wallets/export" }, + { from: "/wallets/hooks/use-export-wallet", to: "/wallets/export" }, + { from: "/wallets/hooks/use-get-user-email", to: "/wallets/quickstart" }, + { from: "/wallets/hooks/use-login-passkey", to: "/wallets/auth/passkeys" }, + { from: "/wallets/hooks/use-refresh-session", to: "/wallets/session-management" }, + { from: "/wallets/hooks/use-register-passkey", to: "/wallets/auth/passkeys" }, + { from: "/wallets/hooks/use-send-magic-link", to: "/wallets/auth/magic-link" }, + { from: "/wallets/hooks/use-send-otp", to: "/wallets/auth/email-otp" }, + { from: "/wallets/hooks/use-verify-magic-link", to: "/wallets/auth/magic-link" }, + { from: "/wallets/hooks/use-verify-otp", to: "/wallets/auth/email-otp" }, + { from: "/wallets/shared/mutation-result", to: "/wallets/quickstart" }, + { from: "/wallets/shared/query-result", to: "/wallets/quickstart" }, + { from: "/wallets/wallet-api/sign-typed-message", to: "/wallets/wallet-api/sign-message" }, + + // /smart-accounts/embedded-wallet/* -> /wallets/* + { from: "/smart-accounts/embedded-wallet", to: "/wallets" }, + { from: "/smart-accounts/embedded-wallet/auth/email-otp", to: "/wallets/auth/email-otp" }, + { from: "/smart-accounts/embedded-wallet/auth/google-oauth", to: "/wallets/auth/google-oauth" }, + { from: "/smart-accounts/embedded-wallet/auth/magic-link", to: "/wallets/auth/magic-link" }, + { from: "/smart-accounts/embedded-wallet/auth/passkeys", to: "/wallets/auth/passkeys" }, + { from: "/smart-accounts/embedded-wallet/export", to: "/wallets/export" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-authenticate-oauth", to: "/wallets/auth/google-oauth" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-export-private-key", to: "/wallets/export" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-export-wallet", to: "/wallets/export" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-get-user-email", to: "/wallets/quickstart" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-login-passkey", to: "/wallets/auth/passkeys" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-refresh-session", to: "/wallets/session-management" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-register-passkey", to: "/wallets/auth/passkeys" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-send-magic-link", to: "/wallets/auth/magic-link" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-send-otp", to: "/wallets/auth/email-otp" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-verify-magic-link", to: "/wallets/auth/magic-link" }, + { from: "/smart-accounts/embedded-wallet/hooks/use-verify-otp", to: "/wallets/auth/email-otp" }, + { from: "/smart-accounts/embedded-wallet/quickstart", to: "/wallets/quickstart" }, + { from: "/smart-accounts/embedded-wallet/session-management", to: "/wallets/session-management" }, + { from: "/smart-accounts/embedded-wallet/shared/mutation-result", to: "/wallets/quickstart" }, + { from: "/smart-accounts/embedded-wallet/shared/query-result", to: "/wallets/quickstart" }, + { from: "/smart-accounts/embedded-wallet/wallet-api/send-transaction", to: "/wallets/wallet-api/send-transaction" }, + { from: "/smart-accounts/embedded-wallet/wallet-api/sign-message", to: "/wallets/wallet-api/sign-message" }, + { from: "/smart-accounts/embedded-wallet/wallet-api/sign-typed-message", to: "/wallets/wallet-api/sign-message" }, + + // Magic Account subpaths with a closer home than the overview + { from: "/magic-account/knowledge-base/capabilities", to: "https://www.zerodev.app/blogs/sdks-are-dead-long-live-capabilities" }, + { from: "/magic-account/knowledge-base/chain-abstraction", to: "/smart-accounts/chain-abstraction/overview" }, + { from: "/magic-account/knowledge-base/permissions", to: "/smart-accounts/permissions/intro" }, + { from: "/magic-account/knowledge-base/smart-account", to: "/onboarding/create-a-smart-account" }, + { from: "/magic-account/quickstart", to: "/get-started/quickstart" }, + { from: "/magic-account/setup", to: "/onboarding/create-a-smart-account" }, + { from: "/magic-account/setup/eoa", to: "/onboarding/eoa" }, + { from: "/magic-account/setup/passkeys", to: "/onboarding/passkeys/overview" }, + { from: "/magic-account/setup/social", to: "/onboarding/social-login" }, + { from: "/magic-account/setup/third-party-signers", to: "/onboarding/auth-providers" }, + { from: "/magic-account/usage", to: "/smart-accounts/send-transactions" }, + { from: "/magic-account/usage/automating-transactions", to: "/smart-accounts/permissions/transaction-automation" }, + { from: "/magic-account/usage/batching-transactions", to: "/smart-accounts/batch-transactions" }, + { from: "/magic-account/usage/chain-abstraction", to: "/smart-accounts/chain-abstraction/overview" }, + { from: "/magic-account/usage/one-click-trading", to: "/smart-accounts/permissions/1-click-trading" }, + { from: "/magic-account/usage/paying-gas-with-erc20", to: "/smart-accounts/pay-gas-with-erc20s" }, + { from: "/magic-account/usage/sponsoring-gas", to: "/smart-accounts/sponsor-gas/evm" }, + { from: "/magic-account/why-chain-abstraction", to: "/smart-accounts/chain-abstraction/overview" }, + { from: "/magic-account/yi-vs-others", to: "/smart-accounts/chain-abstraction/overview" }, + + // /modules/* (7579 modules) -> /smart-accounts/* + { from: "/modules", to: "/" }, + { from: "/modules/account-recovery", to: "/advanced/account-recovery/sdk-recovery" }, + { from: "/modules/batching", to: "/smart-accounts/batch-transactions" }, + { from: "/modules/defi-integrations", to: "/smart-accounts/defi" }, + { from: "/modules/gas", to: "/smart-accounts/sponsor-gas/evm" }, + { from: "/modules/importing-assets", to: "/smart-accounts/chain-abstraction/overview" }, + { from: "/modules/one-click-trading", to: "/smart-accounts/permissions/transaction-automation" }, + { from: "/modules/onramp", to: "/onramp/smart-routing-address" }, + { from: "/modules/signin", to: "/onboarding/create-a-smart-account" }, + { from: "/modules/transaction-automation", to: "/smart-accounts/permissions/transaction-automation" }, + + // orphan /smart-wallet/* + { from: "/smart-wallet/batching", to: "/smart-accounts/batch-transactions" }, + { from: "/smart-wallet/gas", to: "/smart-accounts/sponsor-gas/evm" }, + { from: "/smart-wallet/intro", to: "/onboarding/create-a-smart-account" }, + { from: "/smart-wallet/onramp", to: "/onramp/smart-routing-address" }, + { from: "/smart-wallet/setup", to: "/onboarding/create-a-smart-account" }, + { from: "/smart-wallet/signin", to: "/onboarding/create-a-smart-account" }, + + // /kernel/* contract docs + { from: "/kernel/intro", to: "/" }, + + // versioned SDK path + { from: "/sdk/v5_3_x/faqs/why-chain-abstraction", to: "/sdk/v5_3_x/advanced/chain-abstraction" }, ]; diff --git a/resolve-redirect.d.ts b/resolve-redirect.d.ts new file mode 100644 index 0000000..9d4ad80 --- /dev/null +++ b/resolve-redirect.d.ts @@ -0,0 +1,3 @@ +export declare const PREFIX: [string, string][]; +export declare function stripTrailingSlash(pathname: string): string; +export declare function resolveRedirect(pathname: string): string | null; diff --git a/resolve-redirect.js b/resolve-redirect.js new file mode 100644 index 0000000..0ab20fc --- /dev/null +++ b/resolve-redirect.js @@ -0,0 +1,21 @@ +// Shared by server.mjs, the vocs dev middleware, and scripts/check-redirects.mjs. + +import { redirects } from "./redirects.config.js"; + +const EXACT = new Map(redirects.map((r) => [r.from, r.to])); + +// Catch-all for subpaths with no exact rule. +export const PREFIX = [["/magic-account", "/smart-accounts/chain-abstraction/overview"]]; + +export const stripTrailingSlash = (pathname) => + pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname; + +export function resolveRedirect(pathname) { + const clean = stripTrailingSlash(pathname); + return ( + EXACT.get(pathname) ?? + EXACT.get(clean) ?? + PREFIX.find(([p]) => clean === p || clean.startsWith(p + "/"))?.[1] ?? + null + ); +} diff --git a/scripts/check-redirects.mjs b/scripts/check-redirects.mjs new file mode 100644 index 0000000..0334ceb --- /dev/null +++ b/scripts/check-redirects.mjs @@ -0,0 +1,105 @@ +// npm run check:redirects — an audit, run it by hand. Reports a dead target, a +// chain, a duplicate source, a rule (exact or prefix) that shadows a page that +// still exists, or a target no sidebar links to. + +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { redirects } from "../redirects.config.js"; +import { PREFIX, resolveRedirect, stripTrailingSlash } from "../resolve-redirect.js"; + +const ROOT = fileURLToPath(new URL("..", import.meta.url)); +const PAGES = join(ROOT, "docs", "pages"); + +// Derived from page sources, so the check runs without a build. +function routes(dir = PAGES, prefix = "") { + const out = new Set(); + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + for (const r of routes(full, `${prefix}/${entry}`)) out.add(r); + } else if (/\.mdx?$/.test(entry)) { + const name = entry.replace(/\.mdx?$/, ""); + out.add(name === "index" ? prefix || "/" : `${prefix}/${name}`); + } + } + return out; +} + +if (!existsSync(PAGES)) { + console.error(`docs/pages not found at ${PAGES}`); + process.exit(1); +} + +// Routes the sidebar links to. A redirect that lands anywhere else drops the +// reader on a page with no navigation around it. +function sidebarLinks() { + const cfg = readFileSync(join(ROOT, "vocs.config.tsx"), "utf8"); + const start = cfg.indexOf(" sidebar: {"); + if (start < 0) return null; + let depth = 0, end = -1; + for (let i = cfg.indexOf("{", start); i < cfg.length; i++) { + if (cfg[i] === "{") depth++; + else if (cfg[i] === "}" && --depth === 0) { end = i; break; } + } + if (end < 0) return null; + return new Set([...cfg.slice(start, end).matchAll(/link:\s*["'](\/[^"']*)["']/g)] + .map((m) => stripTrailingSlash(m[1]))); +} + +// Orphaned on purpose. The React hooks tree documents @zerodev/waas, which is +// unmaintained: DES-21 settled on keeping the pages for old links but hiding +// them from crawlers, so they stay out of the sidebar. New orphan targets +// outside these must not be added. +const ORPHAN_OK = ["/advanced/react-hooks/", "/api-and-toolings/", "/smart-accounts/permissions/1-click-trading"]; + +const SIDEBAR = sidebarLinks(); +const ROUTES = routes(); +const errors = []; +const seen = new Map(); + +// Fail loudly rather than skipping the reachability check: if the sidebar is +// reformatted or renamed, silence here would read as a pass. +if (!SIDEBAR) errors.push("cannot parse the sidebar block in vocs.config.tsx, so target reachability went unchecked"); + +for (const { from, to } of redirects) { + if (seen.has(from)) errors.push(`duplicate source ${from} (${seen.get(from)} and ${to})`); + seen.set(from, to); + + // Internal targets only: blog posts keep their source files on purpose while + // pointing at the marketing site. + if (!to.startsWith("http") && ROUTES.has(stripTrailingSlash(from))) { + errors.push(`${from} is redirected but the page still exists`); + } + + if (to.startsWith("http")) continue; + + const target = stripTrailingSlash(to.split(/[?#]/)[0]); + if (!ROUTES.has(target)) errors.push(`${from} -> ${to} (no such page)`); + if (resolveRedirect(target)) errors.push(`${from} -> ${to} -> ${resolveRedirect(target)} (chain)`); + + if (SIDEBAR && target !== "/" && !SIDEBAR.has(target) && !ORPHAN_OK.some((p) => target.startsWith(p))) { + errors.push(`${from} -> ${to} (no sidebar links to this page)`); + } +} + +for (const [prefix, target] of PREFIX) { + if (!ROUTES.has(stripTrailingSlash(target))) { + errors.push(`prefix ${prefix} -> ${target} (no such page)`); + } + // A page added under the prefix is unreachable: the redirect answers first. + for (const route of ROUTES) { + if (route === prefix || route.startsWith(prefix + "/")) { + if (!seen.has(route)) errors.push(`prefix ${prefix} shadows ${route}, which still exists`); + } + } +} + +if (errors.length) { + console.error(`${errors.length} redirect problem(s):`); + for (const e of errors) console.error(` ${e}`); + process.exit(1); +} + +console.log(`${redirects.length} redirects ok, ${ROUTES.size} routes`); diff --git a/server.mjs b/server.mjs index 94687dc..aa92751 100644 --- a/server.mjs +++ b/server.mjs @@ -8,17 +8,11 @@ import { fileURLToPath } from "node:url"; import sirv from "sirv"; -import { redirects } from "./redirects.config.js"; +import { resolveRedirect } from "./resolve-redirect.js"; const PORT = Number(process.env.PORT) || 10000; const DIR = join(fileURLToPath(new URL(".", import.meta.url)), "docs", "dist"); -const EXACT = new Map(redirects.map((r) => [r.from, r.to])); -const PREFIX = [ - ["/magic-account", "/smart-accounts/chain-abstraction/overview"], -]; -const SPA_FALLBACK = ["/sdk", "/meta-infra", "/recovery-flow", "/react", "/smart-wallet"]; - const assets = sirv(DIR, { etag: true, gzip: true, @@ -33,35 +27,18 @@ const assets = sirv(DIR, { } }, }); -const shell = sirv(DIR, { etag: true, single: true }); // serves index.html on miss const notFound = existsSync(join(DIR, "404.html")) ? readFileSync(join(DIR, "404.html")) : null; -function redirectTo(pathname) { - const clean = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname; - return ( - EXACT.get(pathname) ?? - EXACT.get(clean) ?? - PREFIX.find(([p]) => clean === p || clean.startsWith(p + "/"))?.[1] ?? - null - ); -} - -const isSpaFallback = (pathname) => { - const clean = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname; - return SPA_FALLBACK.some((p) => clean === p || clean.startsWith(p + "/")); -}; - createServer((req, res) => { const { pathname, search } = new URL(req.url, "http://localhost"); - const to = redirectTo(pathname); + const to = resolveRedirect(pathname); if (to) { res.writeHead(301, { location: to + search }); return res.end(); } assets(req, res, () => { - if (isSpaFallback(pathname)) return shell(req, res, () => res.end()); if (notFound) { res.writeHead(404, { "content-type": "text/html; charset=utf-8" }); return res.end(notFound); diff --git a/vocs.config.tsx b/vocs.config.tsx index cae54ee..7c0bc7e 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -1,7 +1,7 @@ import { resolve } from "node:path"; import { defineConfig } from "vocs"; import dotenv from "dotenv"; -import { redirects } from "./redirects.config.js"; +import { resolveRedirect } from "./resolve-redirect.js"; // Absolute path to docs/components, used as the `@components` Vite alias below // so MDX pages can import shared components with a single stable specifier @@ -12,13 +12,6 @@ const componentsDir = resolve(process.cwd(), "docs/components"); dotenv.config(); -// Lookup table for the dev-server redirect middleware below. The redirect list -// lives in redirects.config.js (single source of truth); in production the same -// list is applied by server.mjs (the Render web service). -const REDIRECT_MAP: Record = Object.fromEntries( - redirects.map((r) => [r.from, r.to]), -); - export default defineConfig({ iconUrl: "/favicon.ico", logoUrl: { @@ -1053,11 +1046,12 @@ export default defineConfig({ plugins: [ { name: "ia-revamp-redirects", + // 307 not 301: browsers cache a 301 forever, painful while editing. configureServer(server) { server.middlewares.use((req, res, next) => { if (!req.url) return next(); const url = new URL(req.url, "http://localhost"); - const target = REDIRECT_MAP[url.pathname]; + const target = resolveRedirect(url.pathname); if (target) { res.statusCode = 307; res.setHeader("Location", target + url.search);