designpaca/apps/site/src/layouts/Base.astro
Yun Chan 7ef9106723
All checks were successful
ci / build (push) Successful in 41s
fix(site): add explicit not-found page
2026-08-30 18:24:48 +09:00

307 lines
9.7 KiB
Text

---
import { landingKo, landingEn } from "../i18n/landing";
import socialPreviewKo from "../assets/og-designpaca-ko.png";
import socialPreviewEn from "../assets/og-designpaca-en.png";
import "../styles/fonts.css";
import "../styles/tokens.css";
import "../styles/base.css";
interface Props {
lang: "ko" | "en";
title: string;
description: string;
translationPath?: string | null;
noindex?: boolean;
}
const { lang, title, description, noindex = false } = Astro.props;
const copy = lang === "ko" ? landingKo : landingEn;
const socialPreview = lang === "ko" ? socialPreviewKo : socialPreviewEn;
const socialPreviewAlt = lang === "ko"
? "designpaca의 일곱 단계 디자인 파이프라인 그래픽"
: "The seven-stage designpaca pipeline graphic";
const path = Astro.url.pathname;
const normalizedPath = path.replace(/\/+$/, "") || "/";
const isKoHome = normalizedPath === "/";
const isEnHome = normalizedPath === "/en";
const inferredTranslationPath = isKoHome ? "/en/" : isEnHome ? "/" : null;
const translationPath = Object.prototype.hasOwnProperty.call(Astro.props, "translationPath")
? Astro.props.translationPath
: inferredTranslationPath;
const homePath = lang === "ko" ? "/" : "/en/";
const showcasePath = lang === "ko" ? "/showcase/" : "/en/showcase/";
const isHome = isKoHome || isEnHome;
const navItems = copy.nav.items.map((item) => ({
...item,
href: isHome ? item.href : `${homePath}${item.href}`,
}));
const repositoryUrl = "https://git.chanpaca.net/yunchan/designpaca";
const canonical = new URL(path, Astro.site);
const translated = translationPath ? new URL(translationPath, Astro.site) : null;
const socialPreviewUrl = new URL(socialPreview.src, Astro.site);
const markBg = "%23c43f20";
const markFg = "%23faf9f4";
const favicon = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='3' fill='${markBg}'/%3E%3Cpath d='M8 6v20M8 7h12M8 11h8M8 15h13M8 19h9M8 23h12' stroke='${markFg}' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E`;
const schema = isHome
? {
"@context": "https://schema.org",
"@type": "WebSite",
name: "designpaca",
url: new URL("/", Astro.site).href,
description,
inLanguage: ["ko-KR", "en-US"],
}
: {
"@context": "https://schema.org",
"@type": "WebPage",
name: title,
url: canonical.href,
description,
inLanguage: lang === "ko" ? "ko-KR" : "en-US",
};
---
<!doctype html>
<html lang={lang}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
{noindex && <meta name="robots" content="noindex, follow" />}
<meta name="theme-color" content="#f2f1eb" />
<meta name="color-scheme" content="light" />
<link rel="icon" href={favicon} />
<link rel="canonical" href={canonical} />
{translated && !isHome && <link rel="alternate" hreflang={lang} href={canonical} />}
{translated && !isHome && <link rel="alternate" hreflang={lang === "ko" ? "en" : "ko"} href={translated} />}
{translated && !isHome && <link rel="alternate" hreflang="x-default" href={lang === "ko" ? canonical : translated} />}
{isHome && <link rel="alternate" hreflang="ko" href={new URL("/", Astro.site)} />}
{isHome && <link rel="alternate" hreflang="en" href={new URL("/en/", Astro.site)} />}
{isHome && <link rel="alternate" hreflang="x-default" href={new URL("/", Astro.site)} />}
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap"
media="print"
onload="this.media='all'"
/>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:locale" content={lang === "ko" ? "ko_KR" : "en_US"} />
<meta property="og:image" content={socialPreviewUrl} />
<meta property="og:image:width" content={socialPreview.width} />
<meta property="og:image:height" content={socialPreview.height} />
<meta property="og:image:alt" content={socialPreviewAlt} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={socialPreviewUrl} />
<meta name="twitter:image:alt" content={socialPreviewAlt} />
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
</head>
<body>
<a class="skip" href="#main">{lang === "ko" ? "본문으로 건너뛰기" : "Skip to content"}</a>
<header class="site-header">
<div class="header-inner shell">
<a class="wordmark" href={homePath} translate="no" aria-label={lang === "ko" ? "designpaca 홈" : "designpaca home"}>
<svg class="wordmark-mark" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
<path d="M4 2.5v19M4 3h13M4 6h9M4 9h15M4 12h10M4 15h14M4 18h8M4 21h12" />
</svg>
<span>designpaca</span>
</a>
<nav class="nav-primary" aria-label={copy.nav.menuLabel}>
{navItems.map((item) => <a href={item.href}>{item.label}</a>)}
</nav>
<div class="header-actions">
<a class="header-archive" href={showcasePath}>{copy.nav.showcaseLabel}</a>
{translationPath && (
<a
class="header-lang"
href={translationPath}
hreflang={lang === "ko" ? "en" : "ko"}
lang={lang === "ko" ? "en" : "ko"}
aria-label={copy.nav.langLabel}
>
{lang === "ko" ? "EN" : "KO"}
</a>
)}
<a class="header-repo" href={repositoryUrl} rel="noopener" aria-label={copy.nav.repoLabel}>
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4 12 12 4M6 4h6v6" />
</svg>
</a>
</div>
</div>
</header>
<main id="main" tabindex="-1">
<slot />
</main>
</body>
</html>
<style>
.site-header {
position: sticky;
top: 0;
z-index: 10;
min-height: var(--header-h);
background: var(--surface);
border-bottom: var(--line-width) solid var(--line);
}
.header-inner {
min-height: var(--header-h);
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-6);
}
.wordmark {
display: inline-flex;
align-items: center;
gap: var(--space-3);
min-height: var(--control-h-sm);
font-size: 1rem;
font-weight: var(--weight-medium);
letter-spacing: -0.01em;
text-decoration: none;
}
.wordmark-mark {
flex: none;
color: var(--accent);
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
}
.nav-primary {
display: flex;
align-items: center;
justify-content: center;
gap: clamp(var(--space-3), 1.6vw, var(--space-6));
min-width: 0;
}
.nav-primary a,
.header-actions a {
min-height: var(--control-h-sm);
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--ink-muted);
font-size: var(--step--1);
font-weight: var(--weight-medium);
line-height: 1.2;
text-decoration: none;
white-space: nowrap;
}
.nav-primary a {
position: relative;
padding-inline: var(--space-2);
}
.nav-primary a::after {
content: "";
position: absolute;
right: var(--space-2);
bottom: 0;
left: var(--space-2);
height: 1px;
background: var(--ink);
transform: scaleX(0);
transform-origin: left;
transition: transform var(--dur-quick) var(--ease-out);
}
.nav-primary a:hover,
.nav-primary a:focus-visible,
.header-actions a:hover,
.header-actions a:focus-visible {
color: var(--ink);
}
.nav-primary a:hover::after,
.nav-primary a:focus-visible::after {
transform: scaleX(1);
}
.header-actions {
display: flex;
align-items: center;
gap: var(--space-2);
}
.header-archive {
padding-inline: var(--space-3);
border-left: var(--line-width) solid var(--line);
}
.header-lang,
.header-repo {
min-width: var(--control-h-sm);
padding-inline: var(--space-2);
border: var(--line-width) solid var(--line-strong);
}
.header-repo svg {
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
@media (max-width: 64rem) {
.header-inner { gap: var(--space-3); }
.header-archive { display: none !important; }
}
@media (max-width: 48rem) {
.header-inner {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: 3.5rem 3.25rem;
gap: 0 var(--space-3);
}
.wordmark { grid-column: 1; grid-row: 1; }
.header-actions { grid-column: 2; grid-row: 1; }
.nav-primary {
grid-column: 1 / -1;
grid-row: 2;
justify-content: space-between;
gap: var(--space-1);
border-top: var(--line-width) solid var(--line);
}
.nav-primary a {
flex: 1 1 0;
min-width: 0;
padding-inline: var(--space-1);
text-align: center;
}
.header-repo { display: none !important; }
}
@media (max-width: 23rem) {
.wordmark { gap: var(--space-2); }
.nav-primary a { font-size: 0.7rem; }
}
</style>