From cb7cc9303652c1f98025078ff0912a4fabcbef87 Mon Sep 17 00:00:00 2001 From: Juan Ibarlucea Date: Fri, 12 Jun 2026 21:19:43 -0300 Subject: [PATCH 1/3] fix(seo): redirect stale squareup auth link + defer contact-us support email Follow-up cleanup after the MARTECH-19 re-crawl (health 95, errors 143->39): - Redirect /:locale/references/auth-providers/squareup -> .../square. An external/stale link to the old "squareup" slug 404'd; this clears the "broken redirect" plus its 404/4XX rows. - The contact-us "Email Support" card now assembles its mailto after mount (mirrors ), so SSR markup is a plain contact-page link and Cloudflare can't rewrite it into a broken /cdn-cgi/l/email-protection link. Co-Authored-By: Claude Opus 4.8 --- app/en/resources/contact-us/contact-cards.tsx | 12 ++++++++++-- next.config.ts | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/en/resources/contact-us/contact-cards.tsx b/app/en/resources/contact-us/contact-cards.tsx index 9e2e720a9..1c4cd5ad1 100644 --- a/app/en/resources/contact-us/contact-cards.tsx +++ b/app/en/resources/contact-us/contact-cards.tsx @@ -20,7 +20,7 @@ import { Users, } from "lucide-react"; import posthog from "posthog-js"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { QuickStartCard } from "../../../_components/quick-start-card"; @@ -264,6 +264,14 @@ function SuccessMessage({ onClose }: { onClose: () => void }) { export function ContactCards() { const [isSalesModalOpen, setIsSalesModalOpen] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); + // Assemble the support mailto only after mount so the SSR/crawled markup + // shows a plain contact-page link — Cloudflare's email obfuscation then has + // nothing to rewrite into a broken /cdn-cgi/l/email-protection link. Mirrors + // (see app/_components/contact-email.tsx). + const [supportHref, setSupportHref] = useState("/en/resources/contact-us"); + useEffect(() => { + setSupportHref("mailto:support@arcade.dev"); + }, []); const handleContactSalesClick = () => { posthog.capture("Contact sales modal opened", { @@ -282,7 +290,7 @@ export function ContactCards() {
diff --git a/next.config.ts b/next.config.ts index c0c5b2062..c3347c3d6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -31,6 +31,13 @@ const nextConfig: NextConfig = withLlmsTxt({ destination: "/:locale/resources/integrations", permanent: true, }, + // The auth provider is "square"; an external/stale link points at the + // old "squareup" slug, which 404s. Send it to the real page. + { + source: "/:locale/references/auth-providers/squareup", + destination: "/:locale/references/auth-providers/square", + permanent: true, + }, // Dissolved guides/security section { source: "/:locale/guides/security/security-research-program", From 28050e7f6c864e4a49cee4d38bb05cf25537ac54 Mon Sep 17 00:00:00 2001 From: Juan Ibarlucea Date: Mon, 15 Jun 2026 18:46:44 -0300 Subject: [PATCH 2/3] test(seo): guard toolkit canonical hygiene (docs analog of the www guard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the canonical guard added to ArcadeAI/www on the docs side. docs' only page-level canonical comes from the generated toolkit pages, so tests/integration-index-links.test.ts now asserts (re-deriving the canonical with the same pure helpers generateMetadata uses — readToolkitData + getToolkitSlug — to avoid importing browser-only render code): - every toolkit page's canonical points at its own URL (self-canonical), - canonicals are unique (no duplicate-canonical pages — the notion class MARTECH-19 fixed), - and none points at a redirect source or a non-generated route. The docs sitemap (app/sitemap.ts, static MDX only) is already guarded by tests/sitemap.test.ts (no redirect-source URLs, no duplicates). Co-Authored-By: Claude Opus 4.8 --- tests/integration-index-links.test.ts | 102 +++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/tests/integration-index-links.test.ts b/tests/integration-index-links.test.ts index 637b46a2f..b2f5dd158 100644 --- a/tests/integration-index-links.test.ts +++ b/tests/integration-index-links.test.ts @@ -8,8 +8,13 @@ import { resolveIndexToolkits, toIntegrationLink, } from "@/app/_lib/integration-index"; -import type { ToolkitWithDocsLink } from "@/app/_lib/toolkit-slug"; +import { readToolkitData } from "@/app/_lib/toolkit-data"; import { + getToolkitSlug, + type ToolkitWithDocsLink, +} from "@/app/_lib/toolkit-slug"; +import { + getToolkitStaticParamsForCategory, INTEGRATION_CATEGORIES, listValidIntegrationLinks, } from "@/app/_lib/toolkit-static-params"; @@ -246,3 +251,98 @@ describe("hardcoded internal links in toolkit components resolve", () => { TIMEOUT ); }); + +// --------------------------------------------------------------------------- +// Toolkit page canonical hygiene +// +// docs' only page-level comes from the generated toolkit +// pages (toolkit-docs-page.tsx → generateMetadata, which emits +// `/en/resources/integrations//`). This guards +// that canonical class — the docs analog of the www canonical guard, and +// specifically the "Duplicate pages without canonical" finding MARTECH-19 fixed +// (notion): every toolkit page's canonical points at its own URL, canonicals are +// unique (no two pages share one), and none points at a redirect source or a +// non-generated route. We re-derive the canonical with the same pure helpers the +// page uses (static params + readToolkitData + getToolkitSlug) rather than +// importing the page module, which pulls in browser-only render code. +// +// (The docs sitemap — app/sitemap.ts, static MDX pages only — is guarded in +// tests/sitemap.test.ts: no redirect-source URLs, no duplicates.) +// --------------------------------------------------------------------------- + +describe("toolkit page canonical hygiene", () => { + let canonicals: Array<{ page: string; canonical: string }>; + let validLinks: Set; + let redirectSources: Set; + + beforeAll(async () => { + [validLinks, redirectSources] = await Promise.all([ + listValidIntegrationLinks(), + readRedirectSources(), + ]); + canonicals = []; + for (const category of INTEGRATION_CATEGORIES) { + for (const { toolkitId } of await getToolkitStaticParamsForCategory( + category + )) { + const data = await readToolkitData(toolkitId); + const canonical = data + ? `${INTEGRATIONS}/${category}/${getToolkitSlug({ + id: data.id, + docsLink: data.metadata?.docsLink, + })}` + : ""; + canonicals.push({ + page: `${INTEGRATIONS}/${category}/${toolkitId}`, + canonical, + }); + } + } + }, TIMEOUT); + + test("every generated toolkit page emits a canonical", () => { + expect(canonicals.length).toBeGreaterThan(0); + expect(canonicals.filter((c) => !c.canonical).map((c) => c.page)).toEqual( + [] + ); + }); + + test("each toolkit canonical points at the page's own URL", () => { + const mismatched = canonicals + .filter((c) => c.canonical && c.canonical !== c.page) + .map((c) => `${c.page} → ${c.canonical}`); + expect(mismatched).toEqual([]); + }); + + test("toolkit canonicals are unique (no duplicate-canonical pages)", () => { + const byCanonical = new Map(); + const duplicates: string[] = []; + for (const { page, canonical } of canonicals) { + if (!canonical) { + continue; + } + const prior = byCanonical.get(canonical); + if (prior) { + duplicates.push(`${canonical} ← ${prior} + ${page}`); + } else { + byCanonical.set(canonical, page); + } + } + expect(duplicates).toEqual([]); + }); + + test("no toolkit canonical points at a redirect or a missing route", () => { + const offenders: string[] = []; + for (const { canonical } of canonicals) { + if (!canonical) { + continue; + } + if (redirectSources.has(toLocaleParam(canonical))) { + offenders.push(`${canonical}: redirect source`); + } else if (!validLinks.has(withEnLocale(canonical))) { + offenders.push(`${canonical}: not a generated route`); + } + } + expect(offenders).toEqual([]); + }); +}); From 10604e40cd910237047b26d8fa5c9774095dc035 Mon Sep 17 00:00:00 2001 From: Juan Ibarlucea Date: Mon, 15 Jun 2026 22:29:32 -0300 Subject: [PATCH 3/3] test(seo): fetch toolkit routes once in the canonical guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up: the canonical-hygiene beforeAll looped getToolkitStaticParamsForCategory() over every INTEGRATION_CATEGORIES entry, and that helper recomputes listToolkitRoutes() (toolkit index + every data file) internally — so the full catalog was re-read once per category (~10×), scaling worse as the toolkit count grows. Fetch the routes with a single listToolkitRoutes() call and iterate it directly (it already yields both category and toolkitId). Same coverage; suite ~717ms → ~400ms. Co-Authored-By: Claude Opus 4.8 --- tests/integration-index-links.test.ts | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/integration-index-links.test.ts b/tests/integration-index-links.test.ts index b2f5dd158..e56828c5b 100644 --- a/tests/integration-index-links.test.ts +++ b/tests/integration-index-links.test.ts @@ -14,8 +14,8 @@ import { type ToolkitWithDocsLink, } from "@/app/_lib/toolkit-slug"; import { - getToolkitStaticParamsForCategory, INTEGRATION_CATEGORIES, + listToolkitRoutes, listValidIntegrationLinks, } from "@/app/_lib/toolkit-static-params"; @@ -263,7 +263,7 @@ describe("hardcoded internal links in toolkit components resolve", () => { // (notion): every toolkit page's canonical points at its own URL, canonicals are // unique (no two pages share one), and none points at a redirect source or a // non-generated route. We re-derive the canonical with the same pure helpers the -// page uses (static params + readToolkitData + getToolkitSlug) rather than +// page uses (listToolkitRoutes + readToolkitData + getToolkitSlug) rather than // importing the page module, which pulls in browser-only render code. // // (The docs sitemap — app/sitemap.ts, static MDX pages only — is guarded in @@ -281,22 +281,22 @@ describe("toolkit page canonical hygiene", () => { readRedirectSources(), ]); canonicals = []; - for (const category of INTEGRATION_CATEGORIES) { - for (const { toolkitId } of await getToolkitStaticParamsForCategory( - category - )) { - const data = await readToolkitData(toolkitId); - const canonical = data - ? `${INTEGRATIONS}/${category}/${getToolkitSlug({ - id: data.id, - docsLink: data.metadata?.docsLink, - })}` - : ""; - canonicals.push({ - page: `${INTEGRATIONS}/${category}/${toolkitId}`, - canonical, - }); - } + // Fetch the route list ONCE. getToolkitStaticParamsForCategory() recomputes + // listToolkitRoutes() (toolkit index + every data file) internally, so + // looping it over all categories re-read the whole catalog once per category. + // listToolkitRoutes() already yields both category and toolkitId. + for (const { category, toolkitId } of await listToolkitRoutes()) { + const data = await readToolkitData(toolkitId); + const canonical = data + ? `${INTEGRATIONS}/${category}/${getToolkitSlug({ + id: data.id, + docsLink: data.metadata?.docsLink, + })}` + : ""; + canonicals.push({ + page: `${INTEGRATIONS}/${category}/${toolkitId}`, + canonical, + }); } }, TIMEOUT);