Skip to content

Commit 29c414a

Browse files
juan-arcadedevJuan Ibarluceaclaude
authored
fix(seo): clear residual audit errors — squareup redirect + contact email (MARTECH-19 follow-up) (#1012)
* 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 <ContactEmail>), 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 <noreply@anthropic.com> * test(seo): guard toolkit canonical hygiene (docs analog of the www guard) 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 <noreply@anthropic.com> * test(seo): fetch toolkit routes once in the canonical guard 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 <noreply@anthropic.com> --------- Co-authored-by: Juan Ibarlucea <juanibarlucea@192.168.1.2> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 52cd541 commit 29c414a

3 files changed

Lines changed: 118 additions & 3 deletions

File tree

app/en/resources/contact-us/contact-cards.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
Users,
2121
} from "lucide-react";
2222
import posthog from "posthog-js";
23-
import { useState } from "react";
23+
import { useEffect, useState } from "react";
2424
import { useForm } from "react-hook-form";
2525
import { QuickStartCard } from "../../../_components/quick-start-card";
2626

@@ -264,6 +264,14 @@ function SuccessMessage({ onClose }: { onClose: () => void }) {
264264
export function ContactCards() {
265265
const [isSalesModalOpen, setIsSalesModalOpen] = useState(false);
266266
const [isSubmitted, setIsSubmitted] = useState(false);
267+
// Assemble the support mailto only after mount so the SSR/crawled markup
268+
// shows a plain contact-page link — Cloudflare's email obfuscation then has
269+
// nothing to rewrite into a broken /cdn-cgi/l/email-protection link. Mirrors
270+
// <ContactEmail> (see app/_components/contact-email.tsx).
271+
const [supportHref, setSupportHref] = useState("/en/resources/contact-us");
272+
useEffect(() => {
273+
setSupportHref("mailto:support@arcade.dev");
274+
}, []);
267275

268276
const handleContactSalesClick = () => {
269277
posthog.capture("Contact sales modal opened", {
@@ -282,7 +290,7 @@ export function ContactCards() {
282290
<div className="mt-16 grid gap-8 md:grid-cols-2 lg:grid-cols-3">
283291
<QuickStartCard
284292
description="Get help with technical issues, account questions, and general inquiries from our support team. Email support is available for customers on paid plans."
285-
href="mailto:support@arcade.dev"
293+
href={supportHref}
286294
icon={Mail}
287295
title="Email Support"
288296
/>

next.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ const nextConfig: NextConfig = withLlmsTxt({
3131
destination: "/:locale/resources/integrations",
3232
permanent: true,
3333
},
34+
// The auth provider is "square"; an external/stale link points at the
35+
// old "squareup" slug, which 404s. Send it to the real page.
36+
{
37+
source: "/:locale/references/auth-providers/squareup",
38+
destination: "/:locale/references/auth-providers/square",
39+
permanent: true,
40+
},
3441
// Dissolved guides/security section
3542
{
3643
source: "/:locale/guides/security/security-research-program",

tests/integration-index-links.test.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import {
88
resolveIndexToolkits,
99
toIntegrationLink,
1010
} from "@/app/_lib/integration-index";
11-
import type { ToolkitWithDocsLink } from "@/app/_lib/toolkit-slug";
11+
import { readToolkitData } from "@/app/_lib/toolkit-data";
12+
import {
13+
getToolkitSlug,
14+
type ToolkitWithDocsLink,
15+
} from "@/app/_lib/toolkit-slug";
1216
import {
1317
INTEGRATION_CATEGORIES,
18+
listToolkitRoutes,
1419
listValidIntegrationLinks,
1520
} from "@/app/_lib/toolkit-static-params";
1621

@@ -246,3 +251,98 @@ describe("hardcoded internal links in toolkit components resolve", () => {
246251
TIMEOUT
247252
);
248253
});
254+
255+
// ---------------------------------------------------------------------------
256+
// Toolkit page canonical hygiene
257+
//
258+
// docs' only page-level <link rel="canonical"> comes from the generated toolkit
259+
// pages (toolkit-docs-page.tsx → generateMetadata, which emits
260+
// `/en/resources/integrations/<category>/<getToolkitSlug(data)>`). This guards
261+
// that canonical class — the docs analog of the www canonical guard, and
262+
// specifically the "Duplicate pages without canonical" finding MARTECH-19 fixed
263+
// (notion): every toolkit page's canonical points at its own URL, canonicals are
264+
// unique (no two pages share one), and none points at a redirect source or a
265+
// non-generated route. We re-derive the canonical with the same pure helpers the
266+
// page uses (listToolkitRoutes + readToolkitData + getToolkitSlug) rather than
267+
// importing the page module, which pulls in browser-only render code.
268+
//
269+
// (The docs sitemap — app/sitemap.ts, static MDX pages only — is guarded in
270+
// tests/sitemap.test.ts: no redirect-source URLs, no duplicates.)
271+
// ---------------------------------------------------------------------------
272+
273+
describe("toolkit page canonical hygiene", () => {
274+
let canonicals: Array<{ page: string; canonical: string }>;
275+
let validLinks: Set<string>;
276+
let redirectSources: Set<string>;
277+
278+
beforeAll(async () => {
279+
[validLinks, redirectSources] = await Promise.all([
280+
listValidIntegrationLinks(),
281+
readRedirectSources(),
282+
]);
283+
canonicals = [];
284+
// Fetch the route list ONCE. getToolkitStaticParamsForCategory() recomputes
285+
// listToolkitRoutes() (toolkit index + every data file) internally, so
286+
// looping it over all categories re-read the whole catalog once per category.
287+
// listToolkitRoutes() already yields both category and toolkitId.
288+
for (const { category, toolkitId } of await listToolkitRoutes()) {
289+
const data = await readToolkitData(toolkitId);
290+
const canonical = data
291+
? `${INTEGRATIONS}/${category}/${getToolkitSlug({
292+
id: data.id,
293+
docsLink: data.metadata?.docsLink,
294+
})}`
295+
: "";
296+
canonicals.push({
297+
page: `${INTEGRATIONS}/${category}/${toolkitId}`,
298+
canonical,
299+
});
300+
}
301+
}, TIMEOUT);
302+
303+
test("every generated toolkit page emits a canonical", () => {
304+
expect(canonicals.length).toBeGreaterThan(0);
305+
expect(canonicals.filter((c) => !c.canonical).map((c) => c.page)).toEqual(
306+
[]
307+
);
308+
});
309+
310+
test("each toolkit canonical points at the page's own URL", () => {
311+
const mismatched = canonicals
312+
.filter((c) => c.canonical && c.canonical !== c.page)
313+
.map((c) => `${c.page}${c.canonical}`);
314+
expect(mismatched).toEqual([]);
315+
});
316+
317+
test("toolkit canonicals are unique (no duplicate-canonical pages)", () => {
318+
const byCanonical = new Map<string, string>();
319+
const duplicates: string[] = [];
320+
for (const { page, canonical } of canonicals) {
321+
if (!canonical) {
322+
continue;
323+
}
324+
const prior = byCanonical.get(canonical);
325+
if (prior) {
326+
duplicates.push(`${canonical}${prior} + ${page}`);
327+
} else {
328+
byCanonical.set(canonical, page);
329+
}
330+
}
331+
expect(duplicates).toEqual([]);
332+
});
333+
334+
test("no toolkit canonical points at a redirect or a missing route", () => {
335+
const offenders: string[] = [];
336+
for (const { canonical } of canonicals) {
337+
if (!canonical) {
338+
continue;
339+
}
340+
if (redirectSources.has(toLocaleParam(canonical))) {
341+
offenders.push(`${canonical}: redirect source`);
342+
} else if (!validLinks.has(withEnLocale(canonical))) {
343+
offenders.push(`${canonical}: not a generated route`);
344+
}
345+
}
346+
expect(offenders).toEqual([]);
347+
});
348+
});

0 commit comments

Comments
 (0)