Skip to content

Latest commit

 

History

History
90 lines (81 loc) · 5.74 KB

File metadata and controls

90 lines (81 loc) · 5.74 KB

SEO metadata lives in the head; CSS and fonts are purged and self-hosted

The page <head> (themes/powershell-community/layouts/_default/baseof.html) emits the full technical-SEO surface: a rel=canonical, Open Graph + Twitter cards, article:* tags for posts, and JSON-LD via two partials (partials/schema-site.htmlOrganization + WebSite on the home page, partials/schema-article.htmlArticle + BreadcrumbList on articles and podcast pages). robots.txt (layouts/robots.txt, enabled by enableRobotsTXT) advertises the sitemap. Render-blocking third-party CSS is replaced by a purged, self-hosted Tailwind build and self-hosted Inter.

Two load-bearing constraints shaped the asset half of this decision.

The deploy builds run bare hugo. Both netlify.toml and .github/workflows/deploy.yml build with hugo --minify, not npm run build. So a CSS step wired into npm would never run in CI. The purged stylesheet is therefore a committed artifact (assets/css/tailwind.css, ~35 KB), not a build-time output. It is regenerated by npm run build:css (scripts/build-tailwind.mjs): download the pinned Tailwind 2.2.19 source, build the whole site to a throwaway dir, and purge the source against that output (purgecss.config.cjs). Hugo then minify | fingerprints the committed file at build time so the immutable Cache-Control in netlify.toml is safe.

Purging silently drops any class it cannot find as a literal token — no build error, just elements that render unstyled in production. The config (purgecss.config.cjs) is the only thing between "smaller CSS" and "broken styles," and two parts of it are load-bearing and must not be weakened:

  • The custom defaultExtractor. PurgeCSS's stock extractor splits candidate tokens on : and /, which would shred every Tailwind variant (lg:grid-cols-3, hover:bg-blue-300, w-1/2). The regex keeps those characters; drop or "simplify" it and the site loses its responsive/state utilities sitewide, silently.
  • The safelist. Runtime-toggled classes (Alpine :class, JS classListhidden, transform rotate-180, the YouTube-facade absolute inset-0 …) survive because they appear as literals in the built HTML, but are pinned anyway against markup churn. The genuine gap is classes injected from data that only exists at deploy time — the activity-dot colors written into data/community_stats.json by .github/scripts/fetch-discourse-activity.js (and the deploy.yml fallback), which never appear in the committed build and are pinned explicitly.

Considered options

  • Keep the CDN links (full Tailwind ~2.9 MB + FontAwesome + Prism, all render-blocking; Inter via a @import in an inline <style>) — rejected. The unpurged Tailwind download alone dominates first paint, and an @import is the worst case for blocking when Inter ships in assets/fonts/ already.
  • PostCSS/Tailwind compiled in the Hugo build (css.PostCSS) — rejected. It needs committed node_modules (tailwindcss + postcss) and a JIT migration off v2; the deploy builds don't run npm, so it would not execute in CI anyway.
  • Regenerate via Tailwind v3/v4 — rejected. v3's palette and resets differ from the v2 classes the markup was authored against, risking silent visual drift. Purging the exact v2 file keeps retained rules byte-identical.
  • Purge the committed v2 file against the built site; self-host Inter — chosen. No visual change for used classes, ~99% smaller CSS, one third-party render-blocking origin removed, and the Google Fonts round-trip eliminated.

Consequences

  • Adding a new Tailwind class requires npm run build:css and committing the result. npm run dev serves the same purged file, so a missing class usually shows up locally — but a forgotten regen is only reliably caught in CI (see below). Prism stays on the CDN, so a preconnect is kept for it.
  • A forgotten regen is caught by CI, not just locally. .github/workflows/build.yml runs build:css + build:icons on every PR and fails if the committed assets/css/tailwind.css or assets/css/fontawesome-subset.css differs from a fresh build — added after a w-auto reached production unstyled. It diffs the deterministic generated CSS (the source of truth for which classes/icons are bundled), not the woff2 bytes, which vary across subset-font versions.
  • FontAwesome is now a self-hosted, purged subset. npm run build:icons (scripts/build-icons.mjs) scans the built HTML for fa-* classes, resolves them via FontAwesome's metadata (including FA5-era aliases so legacy names are not silently dropped), and emits assets/css/fontawesome-subset.css + assets/fonts/fa-*-subset.woff2 (~11 KB total vs ~270 KB of CDN webfonts). Same committed-artifact model as the Tailwind bundle, under the same CI guard.
  • A new build-time data class must be added to the safelist. Anything driven by community_stats.json (or future data) that isn't present in committed markup will be purged unless pinned. The status-color palette is already pinned; bg-orange-500 in the deploy.yml fallback is a no-op because orange is not in the stock Tailwind v2 palette — it never rendered, on the CDN or after.
  • Inter ships only weights 400 and 700 (the two files in assets/fonts/). Heavier display weights used by .gotham-black (900) synthesize from 700; this was visually verified on the hero as acceptable. Adding a weight means adding the font file and a matching @font-face.
  • JSON-LD is gated by section. Article/BreadcrumbList render for articles and podcast; a new content section that should carry article schema must be added to the guard in baseof.html and the breadcrumb branch in schema-article.html.