blog.git

blog

~ Personal research notes. blog.sqzer.com
git clone https://github.com/debuman/blog.git

src/layouts/Base.astro (7354B)

      1 ---
      2 import '../styles/fonts.css';
      3 import '../styles/tokens.css';
      4 import '../styles/base.css';
      5 import { hasHangul } from '../lib/hangul';
      6 
      7 interface Props {
      8   title: string;
      9   description?: string;
     10   section?: string;
     11   /**
     12    * Does this page put Hangul on screen? It decides whether the Korean face is preloaded,
     13    * and it is NOT the same question as the document language. /writing/ is an English
     14    * document — heading "Writing", year kickers — that renders Korean titles above the
     15    * fold; `/` takes the answer from about.md's body and /code/ from each project's name
     16    * and body, which no title or description reveals. Pages with no Korean on screen skip the face: /papers/, /talk/
     17    * and /404.html always, `/` and /code/ while about.md and the project descriptions stay
     18    * English.
     19    * Pass it explicitly wherever the Korean text is not already in title or description.
     20    */
     21   ko?: boolean;
     22 }
     23 const { title, description, section = 'slate', ko } = Astro.props;
     24 
     25 /**
     26  * Preload only the faces that block first paint on THIS route.
     27  *
     28  * Measured: the Korean face is 118,136B against 22,300B for Lora, and it is discovered
     29  * last — HTML, then CSS, then layout, then the request. Lora sets every page, so it is
     30  * preloaded everywhere; the Korean face only where the page puts Hangul on screen, which
     31  * is what `ko` answers and what `needsKo` falls back to asking of the title and
     32  * description.
     33  *
     34  * woff2 is already Brotli-compressed, so none of these numbers move on the wire.
     35  */
     36 const needsKo = ko ?? hasHangul(title, description);
     37 const NAV = [
     38   { label: 'About',   href: '/' },
     39   { label: 'Papers',  href: '/papers/' },
     40   { label: 'Talk',    href: '/talk/' },
     41   { label: 'Code',    href: '/code/' },
     42   { label: 'Writing', href: '/writing/' },
     43 ];
     44 const here = Astro.url.pathname;
     45 // An exact match would leave every nav item unmarked on /writing/2026/vtzero/.
     46 // Only '/' compares exactly, so that About is not current on every page.
     47 const isCurrent = (href: string) => (href === '/' ? here === '/' : here.startsWith(href));
     48 
     49 /**
     50  * The header's left slot is the name of the page you are on, not a wordmark. That is the
     51  * whole state mechanic of the reference header, and the reason its nav carries no
     52  * current-item marker at all: /papers reads "Papers", /code reads "Code", and an article
     53  * reads its SECTION — measured on the live reference, /writing/2025/… shows "Writing",
     54  * never the article's own title, which already has an h1 below.
     55  *
     56  * It is derived from NAV and isCurrent rather than from a new prop, so it needs nothing
     57  * of the pages: startsWith already carries /writing/2026/vtzero/ to "Writing". Root is
     58  * excluded from the match because its nav label is "About", which names a page, not the
     59  * site; / and anything unrouted (404) fall through to the site name instead — the
     60  * reference does the same, labelling its own root nav item "Main" while the slot reads
     61  * the site owner's name.
     62  *
     63  * Note this is NOT the `section` prop above: that one is the palette key (is-slate,
     64  * is-writing) and only article pages pass it.
     65  */
     66 const currentNav = NAV.find((n) => n.href !== '/' && isCurrent(n.href));
     67 /* The site owner's name, not the domain. The reference's own root slot reads "Nicholas
     68  * Carlini" where its nav item reads "Main"; this reads the same way against "About". */
     69 const SITE_OWNER = 'Hyukjin An';
     70 const pageName = currentNav?.label ?? SITE_OWNER;
     71 
     72 /* The author's repository browser and social profiles. */
     73 const ELSEWHERE = [
     74   { label: 'GitHub',    href: 'https://oni.sqzer.com' },
     75   { label: 'LinkedIn',  href: 'https://www.linkedin.com/in/sqzer' },
     76   { label: 'Instagram', href: 'https://www.instagram.com/sqzer_x' },
     77 ];
     78 
     79 /* Copyright range. The opening year is fixed; the closing year is the build's, in UTC like
     80    every other date on this site. Stamping it at build time rather than typing 2026 means the
     81    notice cannot quietly go stale — and if a year passes with no push, it correctly keeps
     82    showing the year of the last publication rather than a year nothing was published in. */
     83 const SITE_YEAR_FROM = 2009;
     84 const buildYear = new Date().getUTCFullYear();
     85 ---
     86 <!doctype html>
     87 <html lang="en">
     88   <head>
     89     <meta charset="utf-8" />
     90     <meta name="viewport" content="width=device-width, initial-scale=1" />
     91     <title>{title}</title>
     92     {description && <meta name="description" content={description} />}
     93     <link rel="canonical" href={new URL(here, Astro.site)} />
     94     <link rel="alternate" type="application/rss+xml" title="debuman" href="/index.xml" />
     95     {/* The icon is the author's own portrait, cropped square on the head rather than
     96         scaled from the 4:5 original — a whole portrait shrunk to 16px is a smudge, where
     97         a head fills the frame and still reads.
     98         No SVG icon: this is a photograph, and the .ico carries 16, 32 and 48 for the
     99         sizes that actually get asked for. */}
    100     <link rel="icon" href="/favicon.ico" sizes="48x48" />
    101     <link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32" />
    102     <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
    103     {/* One Latin face, so this is unconditional: every page paints Lora. The Korean
    104         face follows needsKo. */}
    105     <link rel="preload" href="/fonts/latin-9bba8f25.woff2" as="font" type="font/woff2" crossorigin />
    106     {needsKo && <link rel="preload" href="/fonts/ko-02f8cebd.woff2" as="font" type="font/woff2" crossorigin />}
    107   </head>
    108   <body class={`is-${section}`}>
    109     <a class="skip" href="#main">Skip to content</a>
    110     <header>
    111       {/* A span, not a link and not a heading. Once the slot reads "Papers" an href="/"
    112           on it is a link whose visible name is not its destination, and the same control
    113           would take five different names across the site; the reference's own slot is a
    114           plain div for that reason. / is already in the nav as "About". Not a heading
    115           either: /papers/, /talk/, /code/ and /writing/ each already carry an h1 holding
    116           exactly this word, and a second one inside the banner landmark would duplicate
    117           it. */}
    118       <span class="page-name">{pageName}</span>
    119       {/* Named, as the page's other navs are. "page" only on the page itself: on an
    120           article, Writing is the section it belongs to but a link to another page, and
    121           "true" says exactly that. */}
    122       <nav aria-label="Primary">
    123         {NAV.map((n) => (
    124           <a href={n.href} aria-current={here === n.href ? 'page' : isCurrent(n.href) ? 'true' : undefined}>{n.label}</a>
    125         ))}
    126       </nav>
    127     </header>
    128     <main id="main"><slot /></main>
    129     <footer class="site-foot">
    130       <p>Copyright &copy; {SITE_YEAR_FROM}&ndash;{buildYear} {SITE_OWNER}</p>
    131       {/* rel="me" because these are identity claims, not references — it is what lets a
    132           profile link back and have the pair mean something. No target="_blank": the site
    133           opens nothing in a new tab anywhere else, and the footer is not the place to
    134           start. Text rather than the icons sqzer.com uses, because this design has no
    135           icon vocabulary in its chrome and one pair of glyphs would be the whole of it. */}
    136       <nav class="site-foot__social" aria-label="Elsewhere">
    137         {ELSEWHERE.map((n) => <a href={n.href} rel="me">{n.label}</a>)}
    138       </nav>
    139     </footer>
    140   </body>
    141 </html>