Skip to content

Commit b412608

Browse files
Audit and fix internal links
1 parent 164fa94 commit b412608

8 files changed

Lines changed: 141 additions & 13 deletions

File tree

event-page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@
539539
<section class="section not-found">
540540
<p class="eyebrow dark">Event not found</p>
541541
<h1>This Blue Moon event link does not exist.</h1>
542-
<a class="button primary" href="index.html#events">See upcoming events</a>
542+
<a class="button primary" href="/#events">See upcoming events</a>
543543
</section>
544544
`;
545545
}

event.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
<script src="/site.js?v=4"></script>
6060
<script src="/events.js?v=6"></script>
6161
<script src="/account.js?v=2"></script>
62-
<script src="/member-utils.js?v=3"></script>
63-
<script src="/event-page.js?v=13"></script>
62+
<script src="/member-utils.js?v=4"></script>
63+
<script src="/event-page.js?v=14"></script>
6464
</body>
6565
</html>

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ <h2>Join early access.</h2>
477477
<script src="site.js?v=4"></script>
478478
<script src="events.js?v=6"></script>
479479
<script src="account.js?v=2"></script>
480-
<script src="member-utils.js?v=3"></script>
480+
<script src="member-utils.js?v=4"></script>
481481
<script src="app.js?v=14"></script>
482482
</body>
483483
</html>

member-page.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121
function memberIdFromLocation() {
2222
const params = new URLSearchParams(window.location.search);
23-
return params.get("id") || "";
23+
const fromQuery = params.get("id");
24+
if (fromQuery) return fromQuery;
25+
const match = window.location.pathname.match(/\/members\/([^/]+)/);
26+
return match ? decodeURIComponent(match[1]) : "";
2427
}
2528

2629
function activityItem(event, label) {
@@ -145,7 +148,7 @@
145148
<section class="section not-found">
146149
<p class="eyebrow dark">Member not found</p>
147150
<h1>This Blue Moon member page does not exist yet.</h1>
148-
<a class="button primary" href="index.html#account">Look up members</a>
151+
<a class="button primary" href="/#account">Look up members</a>
149152
</section>
150153
`;
151154
}

member-utils.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,21 @@
102102
];
103103
}
104104

105+
function localPageHref(pageName, id) {
106+
const query = `?id=${encodeURIComponent(id)}`;
107+
return window.location.protocol === "file:" ? `${pageName}${query}` : `/${pageName}${query}`;
108+
}
109+
105110
function eventHref(event) {
106111
const isLocal = ["localhost", "127.0.0.1", ""].includes(window.location.hostname);
107-
if (isLocal) return `event.html?id=${encodeURIComponent(event.id)}`;
112+
if (isLocal) return localPageHref("event.html", event.id);
108113
return `/events/${encodeURIComponent(event.id)}`;
109114
}
110115

111116
function memberHref(member) {
112-
const id = encodeURIComponent(memberId(member.email));
117+
const id = memberId(member.email);
113118
const isLocal = ["localhost", "127.0.0.1", ""].includes(window.location.hostname);
114-
return isLocal ? `member.html?id=${id}` : `/member?id=${id}`;
119+
return isLocal ? localPageHref("member.html", id) : `/members/${encodeURIComponent(id)}`;
115120
}
116121

117122
function members() {

member.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
<script src="/site.js?v=4"></script>
5757
<script src="/events.js?v=6"></script>
5858
<script src="/account.js?v=2"></script>
59-
<script src="/member-utils.js?v=3"></script>
60-
<script src="/member-page.js?v=3"></script>
59+
<script src="/member-utils.js?v=4"></script>
60+
<script src="/member-page.js?v=4"></script>
6161
</body>
6262
</html>

scripts/browser-smoke.mjs

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ function startStaticServer() {
8080
const url = new URL(request.url, "http://127.0.0.1");
8181
let pathname = decodeURIComponent(url.pathname);
8282
if (pathname === "/") pathname = "/index.html";
83-
if (/^\/events\/[^/]+$/.test(pathname)) pathname = "/event.html";
84-
if (/^\/members\/[^/]+$/.test(pathname)) pathname = "/member.html";
83+
if (/^\/events\/[^/]+$/.test(pathname) && !path.extname(pathname)) pathname = "/event.html";
84+
if (/^\/members\/[^/]+$/.test(pathname) && !path.extname(pathname)) pathname = "/member.html";
8585
if (pathname === "/event") pathname = "/event.html";
8686
if (pathname === "/member") pathname = "/member.html";
8787

@@ -380,6 +380,88 @@ async function main() {
380380
});
381381
}
382382

383+
async function auditCurrentPageInternalTargets(label) {
384+
const broken = await evaluateFunction(async () => {
385+
const blockedProtocols = /^(blob|data|javascript|mailto|sms|tel):/i;
386+
const seen = new Set();
387+
const targets = [];
388+
const currentWithoutHash = new URL(window.location.href);
389+
currentWithoutHash.hash = "";
390+
391+
function addTarget(node, attribute, type) {
392+
const raw = node.getAttribute(attribute);
393+
if (!raw || blockedProtocols.test(raw.trim())) return;
394+
395+
let url;
396+
try {
397+
url = new URL(raw, document.baseURI);
398+
} catch (error) {
399+
targets.push({ error: error.message, raw, type });
400+
return;
401+
}
402+
403+
if (url.origin !== window.location.origin) return;
404+
405+
const hash = url.hash;
406+
url.hash = "";
407+
const href = url.toString();
408+
const key = `${type}:${href}:${hash}`;
409+
if (seen.has(key)) return;
410+
seen.add(key);
411+
targets.push({ href, hash, raw, type });
412+
}
413+
414+
[
415+
["a[href]", "href", "anchor"],
416+
["area[href]", "href", "area"],
417+
["form[action]", "action", "form"],
418+
["iframe[src]", "src", "iframe"],
419+
["img[src]", "src", "image"],
420+
["link[href]", "href", "link"],
421+
["script[src]", "src", "script"],
422+
["source[src]", "src", "source"],
423+
["audio[src]", "src", "audio"],
424+
["video[src]", "src", "video"]
425+
].forEach(([selector, attribute, type]) => {
426+
document.querySelectorAll(selector).forEach((node) => addTarget(node, attribute, type));
427+
});
428+
429+
const failures = [];
430+
for (const target of targets) {
431+
if (target.error) {
432+
failures.push(target);
433+
continue;
434+
}
435+
436+
if (target.hash && target.href === currentWithoutHash.toString()) {
437+
const id = decodeURIComponent(target.hash.slice(1));
438+
if (id && !document.getElementById(id) && document.getElementsByName(id).length === 0) {
439+
failures.push({ ...target, error: `Missing fragment target ${target.hash}` });
440+
}
441+
}
442+
443+
let response;
444+
try {
445+
response = await fetch(target.href, { cache: "no-store", method: "HEAD" });
446+
if (response.status === 405) {
447+
response = await fetch(target.href, { cache: "no-store", method: "GET" });
448+
}
449+
} catch (error) {
450+
failures.push({ ...target, error: error.message });
451+
continue;
452+
}
453+
454+
if (!response.ok) {
455+
failures.push({ ...target, status: response.status });
456+
}
457+
}
458+
459+
return failures;
460+
});
461+
462+
assert.deepEqual(broken, [], `${label} should not expose broken internal links or assets.`);
463+
}
464+
383465
try {
384466
await cdp.ready;
385467
const { targetId } = await cdp.send("Target.createTarget", { url: "about:blank" });
@@ -469,6 +551,7 @@ async function main() {
469551

470552
return true;
471553
});
554+
await auditCurrentPageInternalTargets("home page");
472555

473556
await navigate("/events/santa-monica-beach-cleanup", "#event-page");
474557
await evaluateFlow(async (helpers) => {
@@ -478,6 +561,7 @@ async function main() {
478561
assertBrowser(text("#event-page").includes("Example event"), "Seed event pages should be marked as examples.");
479562
return true;
480563
});
564+
await auditCurrentPageInternalTargets("clean event page");
481565

482566
await navigate("/event.html?id=santa-monica-beach-cleanup", "#event-page");
483567
await evaluateFlow(async (helpers) => {
@@ -516,6 +600,7 @@ async function main() {
516600

517601
return true;
518602
});
603+
await auditCurrentPageInternalTargets("event join page");
519604

520605
await navigate("/index.html#account", "#account-dashboard");
521606
await evaluateFlow(async (helpers) => {
@@ -534,6 +619,34 @@ async function main() {
534619
assertBrowser(text("#member-page").includes("Santa Monica Beach Cleanup"), "Member profile should list joined event activity.");
535620
return true;
536621
});
622+
await auditCurrentPageInternalTargets("member query page");
623+
624+
await navigate(`/members/${memberId("riley@example.com")}`, "#member-page");
625+
await evaluateFlow(async (helpers) => {
626+
const { assertBrowser, text, waitUntil } = helpers;
627+
await waitUntil(() => text("#member-page").includes("Riley Stone"), "clean member profile");
628+
assertBrowser(text("#member-page").includes("Santa Monica Beach Cleanup"), "Clean member URL should list joined event activity.");
629+
return true;
630+
});
631+
await auditCurrentPageInternalTargets("clean member page");
632+
633+
await navigate("/events/not-a-real-event", "#event-page");
634+
await evaluateFlow(async (helpers) => {
635+
const { assertBrowser, text } = helpers;
636+
assertBrowser(text("#event-page").includes("Event not found"), "Missing clean event URLs should render the not-found state.");
637+
assertBrowser(Boolean(document.querySelector('a[href="/#events"]')), "Missing event links should return to the root events section.");
638+
return true;
639+
});
640+
await auditCurrentPageInternalTargets("event not-found page");
641+
642+
await navigate(`/members/${memberId("missing@example.com")}`, "#member-page");
643+
await evaluateFlow(async (helpers) => {
644+
const { assertBrowser, text } = helpers;
645+
assertBrowser(text("#member-page").includes("Member not found"), "Missing clean member URLs should render the not-found state.");
646+
assertBrowser(Boolean(document.querySelector('a[href="/#account"]')), "Missing member links should return to the root account section.");
647+
return true;
648+
});
649+
await auditCurrentPageInternalTargets("member not-found page");
537650

538651
await navigate("/event.html?id=santa-monica-beach-cleanup", "#event-page");
539652
await evaluateFlow(async (helpers) => {

scripts/check.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ function checkSeoMetadata() {
172172
const index = readFileSync(path.join(root, "index.html"), "utf8");
173173
const event = readFileSync(path.join(root, "event.html"), "utf8");
174174
const member = readFileSync(path.join(root, "member.html"), "utf8");
175+
const eventPage = readFileSync(path.join(root, "event-page.js"), "utf8");
176+
const memberPage = readFileSync(path.join(root, "member-page.js"), "utf8");
177+
const memberUtils = readFileSync(path.join(root, "member-utils.js"), "utf8");
175178
const robots = readFileSync(path.join(root, "robots.txt"), "utf8");
176179
const sitemap = readFileSync(path.join(root, "sitemap.xml"), "utf8");
177180
const vercelConfig = JSON.parse(readFileSync(path.join(root, "vercel.json"), "utf8"));
@@ -186,6 +189,10 @@ function checkSeoMetadata() {
186189
assert.match(index, /href="\/event\?id=santa-monica-beach-cleanup"/);
187190
assert.match(event, /<title>Blue Moon Beige Event<\/title>/);
188191
assert.match(member, /<title>Blue Moon Beige Member<\/title>/);
192+
assert.ok(eventPage.includes('href="/#events"'), "event not-found pages should link back to the root events section");
193+
assert.ok(memberPage.includes('href="/#account"'), "member not-found pages should link back to the root account section");
194+
assert.ok(memberPage.includes("window.location.pathname.match(/\\/members\\/([^/]+)/)"), "clean member URLs should be readable in the browser");
195+
assert.ok(memberUtils.includes("return isLocal ? localPageHref(\"member.html\", id) : `/members/${encodeURIComponent(id)}`;"), "production member links should use the clean member route");
189196
assert.match(index, /<link rel="canonical" href="https:\/\/bluemoonbeige\.vercel\.app\/">/);
190197
assert.match(index, /<link rel="alternate" hreflang="x-default" href="https:\/\/bluemoonbeige\.vercel\.app\/">/);
191198
const structuredDataMatch = index.match(/<script id="blue-moon-structured-data" type="application\/ld\+json">\s*([\s\S]*?)\s*<\/script>/);

0 commit comments

Comments
 (0)