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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
"prebuild": "bun run generate:changelog && bun run scripts/copy-docs-images.cjs",
"build": "NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:cf": "bun run build",
"build:cf:staging": "NODE_OPTIONS=--max-old-space-size=8192 CLOUDFLARE_ENV=staging vite build",
"sync:mixedbread": "mxbai vs sync $MIXEDBREAD_STORE_ID './content/docs' --ci",
"preview": "vite preview",
"preview:cf": "bun run build:cf && vite preview",
"deploy": "bun run build:cf && wrangler deploy",
"deploy:staging": "bun run build:cf && wrangler deploy --env staging",
"deploy:staging": "bun run build:cf:staging && wrangler deploy",
"cf:typegen": "wrangler types",
"types:check": "fumadocs-mdx && tsc --noEmit",
"postinstall": "fumadocs-mdx",
Expand Down
3 changes: 2 additions & 1 deletion src/components/SearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "fumadocs-ui/components/dialog/search";
import { useDocsSearch } from "fumadocs-core/search/client";
import type { SharedProps } from "fumadocs-ui/contexts/search";
import { buildDocsApiPath } from "@/lib/url-base";

const tags = [
{ name: "iOS", value: "ios" },
Expand All @@ -28,7 +29,7 @@ export function CustomSearchDialog(props: SharedProps) {
const [tag, setTag] = useState<string | undefined>();
const { search, setSearch, query } = useDocsSearch({
type: "fetch",
api: "/docs/api/search",
api: buildDocsApiPath("search"),
tag,
});

Expand Down
3 changes: 2 additions & 1 deletion src/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Markdown } from "./markdown";
import { Presence } from "@radix-ui/react-presence";
import { useLocalStorage } from "../hooks/useLocalStorage";
import { SDK_OPTIONS } from "../lib/sdk-options";
import { buildDocsApiPath } from "../lib/url-base";

const API_URL = "https://docs-ai-api.superwall.com";

Expand Down Expand Up @@ -268,7 +269,7 @@ export function AISearch({ children }: { children: ReactNode }) {
messagesRef.current.slice(0, idx).reverse().find((m) => m.role === "user")?.content ??
"";

fetch("/docs/api/feedback", {
fetch(buildDocsApiPath("feedback"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
Expand Down
11 changes: 6 additions & 5 deletions src/lib/layout.shared.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import { CircleHelp } from "lucide-react";
import { buildDocsPath } from "./url-base";

export const gitConfig = {
user: "superwall",
Expand All @@ -10,27 +11,27 @@ export const gitConfig = {
export function baseOptions(): BaseLayoutProps {
return {
nav: {
url: "/docs",
url: buildDocsPath(),
title: (
<>
<img width={150} src="/docs/resources/logo.svg" alt="Superwall" />
<img width={150} src={buildDocsPath("resources/logo.svg")} alt="Superwall" />
</>
),
},
links: [
{
text: "Integrations",
url: "/docs/integrations",
url: buildDocsPath("integrations"),
},
{
text: "Changelog",
url: "/docs/changelog",
url: buildDocsPath("changelog"),
},
{
type: "icon",
icon: <CircleHelp />,
text: "Support",
url: "/docs/support",
url: buildDocsPath("support"),
label: "Support",
},
],
Expand Down
199 changes: 84 additions & 115 deletions src/lib/redirect-route-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,103 +3,72 @@ import { describe, test } from "node:test";
import { externalRedirectsMap, fileRedirectsMap, folderRedirectsMap } from "../../redirects-map";
import { buildRedirectRouteRules } from "./redirect-route-rules";

describe("buildRedirectRouteRules", () => {
test("generates internal file redirect with /docs prefix and 308 status", () => {
const rules = buildRedirectRouteRules();
const rule = rules["/web-checkout-post-checkout-redirecting"];
const expectedDestination = `/docs/${fileRedirectsMap["web-checkout-post-checkout-redirecting"]}`;

assert.ok(rule);
assert.equal(rule.redirect.to, expectedDestination);
assert.equal(rule.redirect.status, 308);
});

test("generates docs-prefixed internal file redirect compatibility rule", () => {
const rules = buildRedirectRouteRules();
const rule = rules["/docs/web-checkout-post-checkout-redirecting"];
const expectedDestination = `/docs/${fileRedirectsMap["web-checkout-post-checkout-redirecting"]}`;

assert.ok(rule);
assert.equal(rule.redirect.to, expectedDestination);
assert.equal(rule.redirect.status, 308);
});
const DOCS_BASE = "/docs";

function normalizeSourcePath(path: string): string {
const trimmed = path.trim();
if (!trimmed) return "/";
return `/${trimmed.replace(/^\/+/, "").replace(/\/+$/, "")}`;
}

function normalizeDocsSourcePath(path: string): string {
const normalized = normalizeSourcePath(path);
if (normalized === DOCS_BASE || normalized.startsWith(`${DOCS_BASE}/`)) return normalized;
if (normalized === "/") return DOCS_BASE;
return `${DOCS_BASE}${normalized}`;
}

function normalizeInternalDestination(path: string): string {
const normalized = normalizeSourcePath(path);
if (normalized === DOCS_BASE || normalized.startsWith(`${DOCS_BASE}/`)) return normalized;
if (normalized === "/") return DOCS_BASE;
return `${DOCS_BASE}${normalized}`;
}

test("generates external redirect with 308 status", () => {
describe("buildRedirectRouteRules", () => {
test("generates docs-scoped internal file redirects with 308 status", () => {
const rules = buildRedirectRouteRules();
const rule = rules["/troubleshooting"];

assert.ok(rule);
assert.equal(
rule.redirect.to,
"https://support.superwall.com/collections/6437438776-troubleshooting",
);
assert.equal(rule.redirect.status, 308);
const source = "web-checkout-post-checkout-redirecting";
const docsKey = normalizeDocsSourcePath(source);
const expectedDestination = normalizeInternalDestination(fileRedirectsMap[source]);

assert.ok(rules[docsKey]);
assert.equal(rules[docsKey].redirect.to, expectedDestination);
assert.equal(rules[docsKey].redirect.status, 308);
assert.equal(rules["/web-checkout-post-checkout-redirecting"], undefined);
});

test("prefers external redirect for conflicting root slug while keeping docs-prefixed internal route", () => {
test("generates docs-scoped external redirects with 308 status", () => {
const rules = buildRedirectRouteRules();
const rootRule = rules["/troubleshooting"];
const docsRule = rules["/docs/troubleshooting"];
const docsKey = normalizeDocsSourcePath("troubleshooting");

assert.ok(rootRule);
assert.ok(docsRule);
assert.ok(rules[docsKey]);
assert.equal(
rootRule.redirect.to,
rules[docsKey].redirect.to,
"https://support.superwall.com/collections/6437438776-troubleshooting",
);
assert.equal(docsRule.redirect.to, "/docs/support/troubleshooting");
});

test("generates docs double-prefix guard redirect", () => {
const rules = buildRedirectRouteRules();
const rule = rules["/docs/docs/**"];

assert.ok(rule);
assert.equal(rule.redirect.to, "/docs/**");
assert.equal(rule.redirect.status, 308);
});

test("includes legacy rn expo one-off redirect", () => {
const rules = buildRedirectRouteRules();
const rule = rules["/installation-via-rn-expo"];

assert.ok(rule);
assert.equal(rule.redirect.to, "/installation-via-expo");
assert.equal(rule.redirect.status, 308);
assert.equal(rules[docsKey].redirect.status, 308);
assert.equal(rules["/troubleshooting"], undefined);
});

test("includes Ask AI compatibility redirects", () => {
test("includes docs compatibility one-off redirects", () => {
const rules = buildRedirectRouteRules();
const rootRule = rules["/ai"];
const docsRule = rules["/docs/ai"];

assert.ok(rootRule);
assert.ok(docsRule);
assert.equal(rootRule.redirect.to, "/docs/support");
assert.equal(docsRule.redirect.to, "/docs/support");
assert.equal(rootRule.redirect.status, 308);
assert.equal(docsRule.redirect.status, 308);
});

test("redirects root path to /docs", () => {
const rules = buildRedirectRouteRules();
const rule = rules["/"];

assert.ok(rule);
assert.equal(rule.redirect.to, "/docs");
assert.equal(rule.redirect.status, 308);
assert.equal(rules["/docs/docs/**"]?.redirect.to, "/docs/**");
assert.equal(rules["/docs/installation-via-rn-expo"]?.redirect.to, "/docs/installation-via-expo");
assert.equal(rules["/docs/ai"]?.redirect.to, "/docs/support");
});

test("redirects docs-prefixed home path to dashboard", () => {
test("does not generate root compatibility redirects", () => {
const rules = buildRedirectRouteRules();
const rule = rules["/docs/home"];

assert.ok(rule);
assert.equal(rule.redirect.to, "/docs/dashboard");
assert.equal(rule.redirect.status, 308);
assert.equal(rules["/"], undefined);
assert.equal(rules["/ai"], undefined);
assert.equal(rules["/installation-via-rn-expo"], undefined);
assert.equal(rules["/home"], undefined);
});

test("generates folder redirects for each slug", () => {
test("generates folder redirects as docs-scoped only", () => {
const rules = buildRedirectRouteRules({
folderRedirectsMap: {
sdk: ["getting-started"],
Expand All @@ -108,12 +77,13 @@ describe("buildRedirectRouteRules", () => {
externalRedirectsMap: {},
});

assert.ok(rules["/getting-started"]);
assert.equal(rules["/getting-started"].redirect.to, "/docs/sdk/getting-started");
assert.equal(rules["/getting-started"].redirect.status, 308);
assert.ok(rules["/docs/getting-started"]);
assert.equal(rules["/docs/getting-started"].redirect.to, "/docs/sdk/getting-started");
assert.equal(rules["/docs/getting-started"].redirect.status, 308);
assert.equal(rules["/getting-started"], undefined);
});

test("throws on conflicting duplicate source collisions", () => {
test("throws on docs-scoped duplicate source collisions", () => {
assert.throws(
() =>
buildRedirectRouteRules({
Expand All @@ -125,58 +95,57 @@ describe("buildRedirectRouteRules", () => {
},
externalRedirectsMap: {},
}),
/Duplicate redirect source "\/same-slug"/,
/Duplicate redirect source "\/docs\/same-slug"/,
);
});

test("builds rules for every configured file and external redirect key", () => {
test("does not emit framework/internal endpoint redirects", () => {
const rules = buildRedirectRouteRules();
const keys = Object.keys(rules);

assert.equal(rules["/docs/_serverFn"], undefined);
assert.equal(rules["/docs/_serverFn/test"], undefined);
assert.equal(rules["/docs/api"], undefined);
assert.equal(rules["/docs/api/search"], undefined);
assert.equal(keys.some((key) => key.startsWith("/_serverFn")), false);
assert.equal(keys.some((key) => key.startsWith("/api")), false);
});

test("builds rules for every configured redirect map key under /docs scope", () => {
const rules = buildRedirectRouteRules();
const externalSourceKeys = new Set(
Object.keys(externalRedirectsMap).map((source) => normalizeDocsSourcePath(source)),
);

for (const [source, destination] of Object.entries(fileRedirectsMap)) {
const key = `/${source}`;
const docsKey = `/docs/${source}`;
const mappedDestination = `/docs/${destination}`;

if (docsKey !== mappedDestination) {
assert.ok(rules[docsKey]);
const docsKey = normalizeDocsSourcePath(source);
const mappedDestination = normalizeInternalDestination(destination);
if (!externalSourceKeys.has(docsKey) && docsKey !== mappedDestination) {
assert.ok(rules[docsKey], `missing redirect for ${docsKey}`);
assert.equal(rules[docsKey].redirect.status, 308);
assert.equal(rules[docsKey].redirect.to, mappedDestination);
}

if (!externalRedirectsMap[source] && key !== mappedDestination) {
assert.ok(rules[key]);
assert.equal(rules[key].redirect.status, 308);
assert.equal(rules[key].redirect.to, mappedDestination);
}
}

for (const [source, destination] of Object.entries(externalRedirectsMap)) {
const key = `/${source}`;
assert.ok(rules[key]);
assert.equal(rules[key].redirect.status, 308);
assert.equal(rules[key].redirect.to, destination);
}

for (const [folder, slugs] of Object.entries(
folderRedirectsMap as Record<string, readonly string[]>,
)) {
for (const slug of slugs) {
const key = `/${slug}`;
const docsKey = `/docs/${slug}`;
const mappedDestination = `/docs/${folder}/${slug}`;

if (docsKey !== mappedDestination) {
assert.ok(rules[docsKey]);
const docsKey = normalizeDocsSourcePath(slug);
const mappedDestination = normalizeInternalDestination(`${folder}/${slug}`);
if (!externalSourceKeys.has(docsKey) && docsKey !== mappedDestination) {
assert.ok(rules[docsKey], `missing redirect for ${docsKey}`);
assert.equal(rules[docsKey].redirect.status, 308);
assert.equal(rules[docsKey].redirect.to, mappedDestination);
}

if (!externalRedirectsMap[slug] && key !== mappedDestination) {
assert.ok(rules[key]);
assert.equal(rules[key].redirect.status, 308);
assert.equal(rules[key].redirect.to, mappedDestination);
}
}
}

for (const [source, destination] of Object.entries(externalRedirectsMap)) {
const docsKey = normalizeDocsSourcePath(source);
assert.ok(rules[docsKey], `missing external redirect for ${docsKey}`);
assert.equal(rules[docsKey].redirect.status, 308);
assert.equal(rules[docsKey].redirect.to, destination);
}
});
});
Loading