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.html → Organization + WebSite on the home page,
partials/schema-article.html → Article + 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, JSclassList—hidden,transform rotate-180, the YouTube-facadeabsolute 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 intodata/community_stats.jsonby.github/scripts/fetch-discourse-activity.js(and thedeploy.ymlfallback), which never appear in the committed build and are pinned explicitly.
- Keep the CDN links (full Tailwind ~2.9 MB + FontAwesome + Prism, all
render-blocking; Inter via a
@importin an inline<style>) — rejected. The unpurged Tailwind download alone dominates first paint, and an@importis the worst case for blocking when Inter ships inassets/fonts/already. - PostCSS/Tailwind compiled in the Hugo build (
css.PostCSS) — rejected. It needs committednode_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.
- Adding a new Tailwind class requires
npm run build:cssand committing the result.npm run devserves 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.ymlrunsbuild:css+build:iconson every PR and fails if the committedassets/css/tailwind.cssorassets/css/fontawesome-subset.cssdiffers from a fresh build — added after aw-autoreached production unstyled. It diffs the deterministic generated CSS (the source of truth for which classes/icons are bundled), not the woff2 bytes, which vary acrosssubset-fontversions. - FontAwesome is now a self-hosted, purged subset.
npm run build:icons(scripts/build-icons.mjs) scans the built HTML forfa-*classes, resolves them via FontAwesome's metadata (including FA5-era aliases so legacy names are not silently dropped), and emitsassets/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-500in thedeploy.ymlfallback 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/BreadcrumbListrender forarticlesandpodcast; a new content section that should carry article schema must be added to the guard inbaseof.htmland the breadcrumb branch inschema-article.html.