From 7ef910672338f1f29a1d75ac35d6520a5f66c284 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sun, 30 Aug 2026 18:24:48 +0900 Subject: [PATCH] fix(site): add explicit not-found page --- apps/site/src/layouts/Base.astro | 4 +- apps/site/src/pages/404.astro | 213 +++++++++++++++++++++++++++++++ apps/site/tools/site-e2e.mjs | 60 ++++++++- 3 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 apps/site/src/pages/404.astro diff --git a/apps/site/src/layouts/Base.astro b/apps/site/src/layouts/Base.astro index ae285b4..4835f41 100644 --- a/apps/site/src/layouts/Base.astro +++ b/apps/site/src/layouts/Base.astro @@ -11,9 +11,10 @@ interface Props { title: string; description: string; translationPath?: string | null; + noindex?: boolean; } -const { lang, title, description } = Astro.props; +const { lang, title, description, noindex = false } = Astro.props; const copy = lang === "ko" ? landingKo : landingEn; const socialPreview = lang === "ko" ? socialPreviewKo : socialPreviewEn; const socialPreviewAlt = lang === "ko" @@ -67,6 +68,7 @@ const schema = isHome {title} + {noindex && } diff --git a/apps/site/src/pages/404.astro b/apps/site/src/pages/404.astro new file mode 100644 index 0000000..64a8398 --- /dev/null +++ b/apps/site/src/pages/404.astro @@ -0,0 +1,213 @@ +--- +import Base from "../layouts/Base.astro"; + +const title = "페이지를 찾을 수 없습니다 | Page not found | designpaca"; +const description = "요청한 주소를 찾을 수 없습니다. 한국어 또는 영어 홈에서 다시 시작할 수 있습니다. Page not found. Return to the Korean or English home page."; +--- + + +
+
+
+

HTTP 404

+

페이지를 찾을 수 없습니다

+

+ 주소가 바뀌었거나 페이지가 이동했습니다. 아래 홈 화면에서 다시 시작해 주세요. +

+

+ Page not found. The address may have changed or the page may have moved. +

+
+ + +
+
+ + + diff --git a/apps/site/tools/site-e2e.mjs b/apps/site/tools/site-e2e.mjs index ba9902c..58f7563 100644 --- a/apps/site/tools/site-e2e.mjs +++ b/apps/site/tools/site-e2e.mjs @@ -32,8 +32,8 @@ const GEOMETRY_WIDTHS = [320, 390, 768, 1024, 1440, 1920, 2560]; const CAPTURE_WIDTHS = new Set([390, 1440]); const HEIGHTS = new Map([[320, 844], [390, 844], [768, 1024], [1024, 900], [1440, 1000], [1920, 1080], [2560, 1440]]); const AXE_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"]; -const EXPECTED_SCENARIOS = 80; -const EXPECTED_ASSERTIONS = 995; +const EXPECTED_SCENARIOS = 81; +const EXPECTED_ASSERTIONS = 1009; const EXPECTED_CAPTURES = 8; function parseArguments(argv) { @@ -1394,6 +1394,62 @@ async function main() { console.log(`Serving ${serveRoot}`); console.log(`Evidence ${evidenceRoot}\n`); + const notFoundScope = "404 artifact and recovery"; + await results.scenario(notFoundScope, async () => { + const artifact = path.join(serveRoot, "404.html"); + results.equal(notFoundScope, "top-level 404 artifact exists", true, fs.existsSync(artifact)); + const opened = await openPage(browser, server.origin, { path: "/404.html" }, { width: 390 }); + try { + const state = await opened.page.evaluate(() => { + const robots = [...document.querySelectorAll("meta[name='robots']")].map((element) => element.getAttribute("content") || ""); + const h1s = [...document.querySelectorAll("h1")].map((element) => element.textContent?.replace(/\s+/g, " ").trim() || ""); + const englishCopy = [...document.querySelectorAll("[lang='en']")].some((element) => /Page not found\./.test(element.textContent || "")); + const links = [...document.querySelectorAll("main a[href]")].map((element) => ({ + href: element.getAttribute("href"), + lang: element.getAttribute("lang"), + text: element.textContent?.replace(/\s+/g, " ").trim() || "", + })); + const executableScripts = [...document.scripts].filter((script) => { + const type = (script.getAttribute("type") || "").trim().toLowerCase(); + return !["application/ld+json", "application/json", "importmap"].includes(type); + }).length; + return { + lang: document.documentElement.lang, + mainCount: document.querySelectorAll("main").length, + h1s, + robots, + englishCopy, + links, + executableScripts, + viewportWidth: innerWidth, + documentWidth: document.documentElement.scrollWidth, + }; + }); + const axe = await runAxe(opened.page); + results.equal(notFoundScope, "local artifact route responds", 200, opened.status); + results.equal(notFoundScope, "document language is Korean", "ko", state.lang); + results.equal(notFoundScope, "one main landmark", 1, state.mainCount); + results.equal(notFoundScope, "one unique h1", 1, state.h1s.length); + results.equal(notFoundScope, "h1 identifies the missing page", "페이지를 찾을 수 없습니다", state.h1s[0]); + results.equal(notFoundScope, "English explanation is language tagged", true, state.englishCopy); + results.equal(notFoundScope, "one robots directive", 1, state.robots.length); + results.check(notFoundScope, "robots directive prevents indexing", state.robots[0]?.split(",").some((value) => value.trim().toLowerCase() === "noindex"), { + expected: "noindex", + actual: state.robots, + }); + results.check(notFoundScope, "Korean home recovery link exists", state.links.some((link) => link.href === "/" && link.text.endsWith("한국어 홈")), { actual: state.links }); + results.check(notFoundScope, "English home recovery link exists and is language tagged", state.links.some((link) => link.href === "/en/" && link.lang === "en" && link.text === "EN English home"), { actual: state.links }); + results.equal(notFoundScope, "no executable scripts", 0, state.executableScripts); + results.check(notFoundScope, "no horizontal document overflow", state.documentWidth <= state.viewportWidth + 1, { + expected: `<= ${state.viewportWidth + 1}`, + actual: state.documentWidth, + }); + results.empty(notFoundScope, "axe WCAG A/AA violations", axe); + } finally { + await closePage(opened.page); + } + }); + // Four routes across the complete local preflight width matrix. Only the // 390/1440 states create visual evidence; every width gets the full geometry gate. for (const route of ROUTES) {