fix(site): add explicit not-found page
All checks were successful
ci / build (push) Successful in 41s
All checks were successful
ci / build (push) Successful in 41s
This commit is contained in:
parent
82e8c8f8ae
commit
7ef9106723
3 changed files with 274 additions and 3 deletions
|
|
@ -11,9 +11,10 @@ interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
translationPath?: string | null;
|
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 copy = lang === "ko" ? landingKo : landingEn;
|
||||||
const socialPreview = lang === "ko" ? socialPreviewKo : socialPreviewEn;
|
const socialPreview = lang === "ko" ? socialPreviewKo : socialPreviewEn;
|
||||||
const socialPreviewAlt = lang === "ko"
|
const socialPreviewAlt = lang === "ko"
|
||||||
|
|
@ -67,6 +68,7 @@ const schema = isHome
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
{noindex && <meta name="robots" content="noindex, follow" />}
|
||||||
<meta name="theme-color" content="#f2f1eb" />
|
<meta name="theme-color" content="#f2f1eb" />
|
||||||
<meta name="color-scheme" content="light" />
|
<meta name="color-scheme" content="light" />
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
|
|
|
||||||
213
apps/site/src/pages/404.astro
Normal file
213
apps/site/src/pages/404.astro
Normal file
|
|
@ -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.";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Base lang="ko" title={title} description={description} translationPath={null} noindex>
|
||||||
|
<section class="not-found grain" aria-labelledby="not-found-title">
|
||||||
|
<div class="not-found-grid shell">
|
||||||
|
<div class="not-found-copy">
|
||||||
|
<p class="status-code mono">HTTP 404</p>
|
||||||
|
<h1 id="not-found-title">페이지를 찾을 수 없습니다</h1>
|
||||||
|
<p class="not-found-lead">
|
||||||
|
주소가 바뀌었거나 페이지가 이동했습니다. 아래 홈 화면에서 다시 시작해 주세요.
|
||||||
|
</p>
|
||||||
|
<p class="not-found-en" lang="en">
|
||||||
|
Page not found. The address may have changed or the page may have moved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="recovery-map frame" aria-label="홈 화면으로 이동">
|
||||||
|
<div class="recovery-source">
|
||||||
|
<span class="recovery-label">현재 위치 <span lang="en">Current route</span></span>
|
||||||
|
<span class="recovery-value nums" aria-hidden="true">404</span>
|
||||||
|
</div>
|
||||||
|
<div class="recovery-links">
|
||||||
|
<a class="recovery-link" href="/">
|
||||||
|
<span class="recovery-code mono" aria-hidden="true">KO</span>
|
||||||
|
<span>한국어 홈</span>
|
||||||
|
</a>
|
||||||
|
<a class="recovery-link" href="/en/" lang="en">
|
||||||
|
<span class="recovery-code mono" aria-hidden="true">EN</span>
|
||||||
|
<span>English home</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Base>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.not-found {
|
||||||
|
min-height: calc(100dvh - var(--header-h));
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 7fr) minmax(18rem, 5fr);
|
||||||
|
gap: var(--space-7);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-copy {
|
||||||
|
align-self: center;
|
||||||
|
max-width: 40rem;
|
||||||
|
padding-block: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-code {
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: var(--step--1);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
max-width: 11ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-lead,
|
||||||
|
.not-found-en {
|
||||||
|
max-width: var(--measure-narrow);
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-lead {
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-en {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
font-size: var(--step--1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-map {
|
||||||
|
position: relative;
|
||||||
|
min-height: 25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-map::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, transparent calc(50% - 0.5px), var(--line) 50%, transparent calc(50% + 0.5px)),
|
||||||
|
linear-gradient(to bottom, transparent calc(50% - 0.5px), var(--line) 50%, transparent calc(50% + 0.5px));
|
||||||
|
opacity: 0.38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-source,
|
||||||
|
.recovery-links {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-source {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-6);
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-label {
|
||||||
|
color: var(--ink-muted);
|
||||||
|
font-size: var(--step--1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-label span {
|
||||||
|
display: block;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-value {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: clamp(5rem, 12vw, 10rem);
|
||||||
|
font-weight: var(--weight-body);
|
||||||
|
line-height: 0.82;
|
||||||
|
letter-spacing: -0.055em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-links {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
border-top: var(--line-width) solid var(--line-strong);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-link {
|
||||||
|
min-height: 4.75rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
color: var(--ink);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
line-height: 1.25;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-link + .recovery-link {
|
||||||
|
border-left: var(--line-width) solid var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-link:hover,
|
||||||
|
.recovery-link:focus-visible {
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-link:active {
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-code {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 48rem) {
|
||||||
|
.not-found {
|
||||||
|
align-items: start;
|
||||||
|
padding-block: var(--space-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-copy {
|
||||||
|
padding-block: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-map {
|
||||||
|
min-height: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-value {
|
||||||
|
font-size: clamp(5rem, 30vw, 8rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 23rem) {
|
||||||
|
.recovery-links {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-link + .recovery-link {
|
||||||
|
border-top: var(--line-width) solid var(--line-strong);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -32,8 +32,8 @@ const GEOMETRY_WIDTHS = [320, 390, 768, 1024, 1440, 1920, 2560];
|
||||||
const CAPTURE_WIDTHS = new Set([390, 1440]);
|
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 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 AXE_TAGS = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"];
|
||||||
const EXPECTED_SCENARIOS = 80;
|
const EXPECTED_SCENARIOS = 81;
|
||||||
const EXPECTED_ASSERTIONS = 995;
|
const EXPECTED_ASSERTIONS = 1009;
|
||||||
const EXPECTED_CAPTURES = 8;
|
const EXPECTED_CAPTURES = 8;
|
||||||
|
|
||||||
function parseArguments(argv) {
|
function parseArguments(argv) {
|
||||||
|
|
@ -1394,6 +1394,62 @@ async function main() {
|
||||||
console.log(`Serving ${serveRoot}`);
|
console.log(`Serving ${serveRoot}`);
|
||||||
console.log(`Evidence ${evidenceRoot}\n`);
|
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
|
// Four routes across the complete local preflight width matrix. Only the
|
||||||
// 390/1440 states create visual evidence; every width gets the full geometry gate.
|
// 390/1440 states create visual evidence; every width gets the full geometry gate.
|
||||||
for (const route of ROUTES) {
|
for (const route of ROUTES) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue