Skip to content

Commit f9e53ed

Browse files
juan-arcadedevJuan Ibarluceaclaudegithub-actions[bot]
authored
fix(seo): kill sitewide broken email link + stale sitemap page (MARTECH-16) (#1004)
* fix(seo): kill sitewide broken email link and stale sitemap page (MARTECH-16) Two Ahrefs Site Audit errors on docs.arcade.dev: - "Page has links to broken page" (229 pages): the footer rendered a sitewide mailto: that Cloudflare Email Obfuscation rewrites into /cdn-cgi/l/email-protection, which 404s for crawlers. Point the footer mail icon at the internal /en/resources/contact-us page instead. - "3XX redirect in sitemap": a stale logic-extensions/build-your-own page lingered after the contextual-access rename and 308-redirects. Delete it; the redirect rule already exists. Regression tests: - tests/chrome-no-mailto.test.ts: no mailto:/email-protection in shared chrome. - tests/sitemap.test.ts: no sitemap URL matches a redirect source. Note: fully clearing the broken-link error also needs Cloudflare "Email Address Obfuscation" disabled for docs.arcade.dev (infra, out of repo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🤖 Regenerate LLMs.txt * refactor(seo): move change rationale from code comments to the PR The narrative comments added in the previous commit (Cloudflare obfuscation backstory, audit counts, ticket refs) belong in the PR description, not the code. Keep terse functional notes only — no behavior change. 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> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent cb33992 commit f9e53ed

5 files changed

Lines changed: 121 additions & 245 deletions

File tree

app/_components/footer.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,7 @@ const Socials = () => (
5454
>
5555
<Linkedin className="h-6 w-6 text-gray-400 transition-colors duration-150 ease-in-out hover:text-gray-900" />
5656
</a>
57-
<a
58-
href={`mailto:${urlConfig.company.email}`}
59-
rel="noreferrer"
60-
target="_blank"
61-
title="Send us an email"
62-
>
57+
<a href="/en/resources/contact-us" title="Contact us">
6358
<Mail className="h-6 w-6 text-gray-400 transition-colors duration-150 ease-in-out hover:text-gray-900" />
6459
</a>
6560
</div>

app/en/guides/logic-extensions/build-your-own/page.mdx

Lines changed: 0 additions & 237 deletions
This file was deleted.

public/llms.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- git-sha: 1713003ee9eb311132f7e93563edd85b2da42011 generation-date: 2026-06-02T23:47:34.646Z -->
1+
<!-- git-sha: 168dbe9f1ea2583887eac41a6f20344ffa661b9c generation-date: 2026-06-10T18:11:23.362Z -->
22

33
# Arcade
44

@@ -85,7 +85,6 @@ Arcade delivers three core capabilities: Deploy agents even your security team w
8585
- [Build an AI Chatbot with Arcade and Vercel AI SDK](https://docs.arcade.dev/en/get-started/agent-frameworks/vercelai): This documentation page guides users through the process of building a browser-based AI chatbot using the Vercel AI SDK and Arcade tools for Gmail and Slack integration. Users will learn how to set up a Next.js project, manage chat state, and implement authorization
8686
- [Build MCP Server QuickStart](https://docs.arcade.dev/en/get-started/quickstarts/mcp-server-quickstart): The "Build MCP Server QuickStart" documentation provides a step-by-step guide for users to create and run a custom MCP Server using the Arcade MCP framework. It covers prerequisites, installation of necessary tools, server setup, and how to implement and call various
8787
- [Build Your Own Contextual Access Server](https://docs.arcade.dev/en/guides/contextual-access/build-your-own): This documentation page provides a comprehensive guide for users to build their own webhook server that complies with the Contextual Access Webhook contract, defined by an OpenAPI 3.0 specification. It outlines the necessary steps, including generating server stubs in various
88-
- [Build Your Own Extension Server](https://docs.arcade.dev/en/guides/logic-extensions/build-your-own): This documentation page provides a comprehensive guide for users to build their own webhook server that complies with the Logic Extensions contract defined by an OpenAPI 3.0 specification. It outlines the necessary steps to implement the server, including generating code stubs in various
8988
- [Call a tool in your IDE/MCP Client](https://docs.arcade.dev/en/get-started/quickstarts/call-tool-client): This documentation page guides users on how to create and utilize an MCP Gateway within their IDE or MCP Client, enabling them to efficiently call tools from multiple MCP servers for specific workflows. Users will learn to set up the gateway, select relevant tools, and connect
9089
- [Call tools from MCP clients](https://docs.arcade.dev/en/guides/create-tools/tool-basics/call-tools-mcp): This documentation page provides guidance on how to configure MCP clients to call tools from an MCP server, detailing necessary prerequisites and outcomes. Users will learn to set up their clients using the `arcade configure` command, customize transport options, and manage configuration files
9190
- [Calling tools in your agent with Arcade](https://docs.arcade.dev/en/get-started/quickstarts/call-tool-agent): This documentation page provides a comprehensive guide on how to utilize Arcade to enable AI agents to call various hosted tools, such as sending emails or creating documents. Users will learn how to install the Arcade client, set up their environment, and implement workflows that leverage

tests/chrome-no-mailto.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { readFileSync } from "node:fs";
2+
import fg from "fast-glob";
3+
import { expect, test } from "vitest";
4+
5+
const TIMEOUT = 30_000;
6+
7+
// Raw email links (mailto:) and Cloudflare's obfuscated email endpoint.
8+
const FORBIDDEN = /mailto:|\/cdn-cgi\/l\/email-protection/gi;
9+
// Comment forms to blank out before scanning (block/JSX `/* */` and line `//`).
10+
const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
11+
const LINE_COMMENT = /(^|[^:])\/\/.*$/;
12+
const NON_NEWLINE = /[^\n]/g;
13+
14+
/**
15+
* Sitewide chrome (footer, navbar, etc. in app/_components) must not contain a
16+
* raw email link: Cloudflare Email Obfuscation rewrites it into a
17+
* /cdn-cgi/l/email-protection URL that returns 404 to crawlers, so one in
18+
* shared chrome becomes a broken link on every page. Link to
19+
* /en/resources/contact-us instead. In-content email links on individual pages
20+
* are fine and are not scanned here.
21+
*/
22+
test(
23+
"shared components contain no mailto: or email-protection links",
24+
async () => {
25+
const files = await fg("app/_components/**/*.{ts,tsx}");
26+
const offenders: Array<{ file: string; line: number; match: string }> = [];
27+
28+
for (const file of files) {
29+
// Blank out comments (so explanatory prose mentioning these patterns is
30+
// not flagged) while preserving newlines to keep line numbers accurate.
31+
const content = readFileSync(file, "utf-8").replace(BLOCK_COMMENT, (m) =>
32+
m.replace(NON_NEWLINE, " ")
33+
);
34+
const lines = content.split("\n");
35+
36+
for (let i = 0; i < lines.length; i += 1) {
37+
const code = lines[i].replace(LINE_COMMENT, "$1");
38+
const matches = code.match(FORBIDDEN);
39+
if (matches) {
40+
for (const match of matches) {
41+
offenders.push({ file, line: i + 1, match });
42+
}
43+
}
44+
}
45+
}
46+
47+
for (const offender of offenders) {
48+
console.error(
49+
`Forbidden sitewide email link in ${offender.file}:${offender.line} — "${offender.match}". ` +
50+
"Cloudflare rewrites sitewide email into /cdn-cgi/l/email-protection (404 for crawlers). " +
51+
"Link to /en/resources/contact-us instead."
52+
);
53+
}
54+
55+
expect(offenders.length).toBe(0);
56+
},
57+
TIMEOUT
58+
);

tests/sitemap.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,67 @@ test("sitemap lists expected URLs", async () => {
3636
}
3737
});
3838

39+
test("sitemap contains no URL that we redirect away", async () => {
40+
const previousSiteUrl = process.env.SITE_URL;
41+
process.env.SITE_URL = "https://example.test";
42+
43+
try {
44+
const { default: sitemap } = await import("../app/sitemap");
45+
const entries = await sitemap();
46+
const paths = entries.map((entry) =>
47+
entry.url.replace("https://example.test", "")
48+
);
49+
50+
// Every redirect `source` in next.config.ts is a path we 3xx away, so a live
51+
// page must never sit there — otherwise the sitemap ships a redirecting URL
52+
// (Ahrefs flags "3XX redirect in sitemap"). Guards against pages left behind
53+
// after a rename.
54+
const config = readFileSync(join(process.cwd(), "next.config.ts"), "utf-8");
55+
const exactSources = new Set<string>();
56+
const prefixSources: string[] = [];
57+
58+
for (const match of config.matchAll(/source:\s*"([^"]+)"/g)) {
59+
const normalized = match[1]
60+
.replace(/:locale\([^)]*\)/g, "en")
61+
.replace(/:locale/g, "en");
62+
63+
if (normalized.includes("/:")) {
64+
// Wildcard source (e.g. ".../:path*"): match by its static prefix, but
65+
// skip any that collapse to just the locale to avoid matching everything.
66+
const prefix = normalized.slice(0, normalized.indexOf("/:"));
67+
if (prefix.split("/").filter(Boolean).length >= 2) {
68+
prefixSources.push(prefix);
69+
}
70+
} else {
71+
exactSources.add(normalized);
72+
}
73+
}
74+
75+
const offenders = paths.filter(
76+
(path) =>
77+
exactSources.has(path) ||
78+
prefixSources.some(
79+
(prefix) => path === prefix || path.startsWith(`${prefix}/`)
80+
)
81+
);
82+
83+
for (const offender of offenders) {
84+
console.error(
85+
`Sitemap includes ${offender}, which matches a redirect source in next.config.ts. ` +
86+
"Delete the stale page (or remove the redirect) so the sitemap ships no 3XX URLs."
87+
);
88+
}
89+
90+
expect(offenders).toEqual([]);
91+
} finally {
92+
if (previousSiteUrl === undefined) {
93+
process.env.SITE_URL = undefined;
94+
} else {
95+
process.env.SITE_URL = previousSiteUrl;
96+
}
97+
}
98+
});
99+
39100
test("robots.txt references the sitemap", () => {
40101
const robotsPath = join(process.cwd(), "public", "robots.txt");
41102
const robotsContent = readFileSync(robotsPath, "utf-8");

0 commit comments

Comments
 (0)