Skip to content

feat: do framework switching server-side#729

Draft
qw-in wants to merge 1 commit intomainfrom
quinn/eng-129-docs-switching-framework-should-be-done-server-side-by-url
Draft

feat: do framework switching server-side#729
qw-in wants to merge 1 commit intomainfrom
quinn/eng-129-docs-switching-framework-should-be-done-server-side-by-url

Conversation

@qw-in
Copy link
Member

@qw-in qw-in commented Dec 23, 2025

WIP


e45f20d

Introduce the routes and convince myself (via a playwright test) that the content was identical or just about identical. Some spacing has changed as we've removed a number of <div> wrappers. Disregard the framework switchers and next/prev links. Those will be sorted out in future commits.

Arcjet folks you can download the playwright report here (too large for GitHub) and review the diff yourself. It didn't make sense to keep this transitive test around in the repo.

Messy source code of the test
import { expect, test, type Page } from "@playwright/test";

/**
 * Temporary test to validate the migrated pages (?f=<framework>) map correctly.
 */

/**
 * List of frameworks ported from `@/lib/prefs.ts`
 */
export const LEGACY_FRAMEWORKS = [
  { key: "astro", label: "Astro" },
  { key: "bun", label: "Bun" },
  { key: "bun-hono", label: "Bun + Hono" },
  { key: "deno", label: "Deno" },
  { key: "fastify", label: "Fastify" },
  { key: "nest-js", label: "NestJS" },
  { key: "next-js", label: "Next.js" },
  { key: "node-js", label: "Node.js" },
  { key: "node-js-express", label: "Node.js + Express" },
  { key: "node-js-hono", label: "Node.js + Hono" },
  { key: "nuxt", label: "Nuxt" },
  { key: "python-fastapi", label: "Python + FastAPI" },
  { key: "python-flask", label: "Python + Flask" },
  { key: "react-router", label: "React Router" },
  { key: "remix", label: "Remix" },
  { key: "sveltekit", label: "SvelteKit" },
] as const;

/**
 * Export of the pages with the frontmatter `frameworks` field.
 */
export const LEGACY_FRAMEWORK_PAGES = [
  {
    path: "/get-started",
    frameworks: [
      "astro",
      "bun",
      "bun-hono",
      "deno",
      "fastify",
      "nest-js",
      "next-js",
      "node-js",
      "node-js-express",
      "node-js-hono",
      "nuxt",
      "python-fastapi",
      "python-flask",
      "react-router",
      "remix",
      "sveltekit",
    ],
  },
  {
    path: "/bot-protection/quick-start",
    frameworks: [
      "astro",
      "bun",
      "deno",
      "nest-js",
      "next-js",
      "node-js",
      "nuxt",
      "remix",
      "sveltekit",
    ],
  },
  {
    path: "/bot-protection/reference",
    frameworks: [
      "bun",
      "deno",
      "nest-js",
      "next-js",
      "node-js",
      "remix",
      "sveltekit",
    ],
  },
  {
    path: "/email-validation/quick-start",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/email-validation/reference",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/filters/reference",
    frameworks: [
      "bun",
      "deno",
      "nest-js",
      "next-js",
      "node-js",
      "remix",
      "sveltekit",
    ],
  },
  {
    path: "/filters/quick-start",
    frameworks: [
      "astro",
      "bun",
      "deno",
      "fastify",
      "nest-js",
      "next-js",
      "node-js",
      "react-router",
      "remix",
      "sveltekit",
    ],
  },
  {
    path: "/nosecone/quick-start",
    frameworks: ["bun", "deno", "next-js", "node-js", "sveltekit"],
  },
  {
    path: "/rate-limiting/quick-start",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/rate-limiting/reference",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/sensitive-info/quick-start",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/sensitive-info/reference",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/shield/quick-start",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/shield/reference",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/signup-protection/quick-start",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
  {
    path: "/signup-protection/reference",
    frameworks: ["bun", "nest-js", "next-js", "node-js", "remix", "sveltekit"],
  },
] as const;

async function screenshotMainContent(page: Page, name: string) {
  await waitForAstroIslands(page);

  await page.evaluate(() => {
    // Astro dev toolbar interferes with screenshots.
    for (const el of document.querySelectorAll("astro-dev-toolbar")) {
      el.remove();
    }

    // YouTube iframes cause inconsistent screenshots.
    for (const el of document.querySelectorAll("lite-youtube")) {
      el.insertAdjacentHTML(
        "afterend",
        "<p>Youtube video removed for screenshot test</p>",
      );
      el.remove();
    }

    // Giscus iframes cause inconsistent screenshots.
    for (const el of document.querySelectorAll("div.giscus")) {
      el.insertAdjacentHTML(
        "afterend",
        "<p>Giscus comments removed for screenshot test</p>",
      );
      el.remove();
    }
  });

  await expect(page.locator("main")).toHaveScreenshot(name, {
    // Using absolute threshold rather than a ration seems to be more
    // consistent for these large text-heavy screenshots.
    maxDiffPixels: 100,
    threshold: 0.1,
  });
}

for (const { path, frameworks } of LEGACY_FRAMEWORK_PAGES) {
  test.describe(`Migrated page ${path}`, () => {
    for (const framework of frameworks) {
      test(`"?f=${framework}" => "${frameworkKeyToPath(framework)}" should match`, async ({ page }) => {
        const legacyResponse = await page.goto(
          `https://docs.arcjet.com${path}?f=${framework}`,
          {
            waitUntil: "networkidle",
          },
        );

        expect(legacyResponse?.ok()).toBeTruthy();

        const screenshotName = `migrate-${path}-${framework}.png`;

        await screenshotMainContent(page, screenshotName);

        // Now we verify that the migrated page matches
        const newResponse = await page.goto(
          `${frameworkKeyToPath(framework)}${path}`,
          {
            waitUntil: "networkidle",
          },
        );

        expect(newResponse?.ok()).toBeTruthy();

        await screenshotMainContent(page, screenshotName);
      });
    }
  });
}

export async function waitForAstroIslands(page: Page, timeout = 15_000) {
  await page.waitForFunction(
    () => {
      const islands = Array.from(document.querySelectorAll("astro-island"));
      // no islands = nothing to wait for
      if (islands.length === 0) return true;

      // hydrated when SSR marker is gone
      return islands.every((el) => !el.hasAttribute("ssr"));
    },
    { timeout },
  );
}

function frameworkKeyToPath(key: string): string {
  switch (key) {
    case "astro":
      return "/sdk/astro";
    case "bun":
      return "/sdk/bun";
    case "bun-hono":
      return "/sdk/bun/plus/hono";
    case "deno":
      return "/sdk/deno";
    case "fastify":
      return "/sdk/fastify";
    case "nest-js":
      return "/sdk/nestjs";
    case "next-js":
      return "/sdk/nextjs";
    case "node-js":
      return "/sdk/nodejs";
    case "node-js-express":
      return "/sdk/nodejs/plus/express";
    case "node-js-hono":
      return "/sdk/nodejs/plus/hono";
    case "nuxt":
      return "/sdk/nuxt";
    case "python-fastapi":
      return "/sdk/python/plus/fastapi";
    case "python-flask":
      return "/sdk/python/plus/flask";
    case "react-router":
      return "/sdk/react-router";
    case "remix":
      return "/sdk/remix";
    case "sveltekit":
      return "/sdk/sveltekit";
    default:
      throw new Error(`Unknown framework key: ${key}`);
  }
}

@qw-in qw-in self-assigned this Dec 23, 2025
@vercel
Copy link

vercel bot commented Dec 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
arcjet-docs Ready Ready Preview, Comment Dec 23, 2025 8:16pm

@davidmytton
Copy link
Contributor

👍 will you also switch the URLs to paths vs query params in this PR?

@qw-in
Copy link
Member Author

qw-in commented Dec 29, 2025

@davidmytton yeah that's the goal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments