|
| 1 | +# SEO metadata lives in the head; CSS and fonts are purged and self-hosted |
| 2 | + |
| 3 | +The page `<head>` (`themes/powershell-community/layouts/_default/baseof.html`) |
| 4 | +emits the full technical-SEO surface: a `rel=canonical`, Open Graph + Twitter |
| 5 | +cards, `article:*` tags for posts, and JSON-LD via two partials |
| 6 | +(`partials/schema-site.html` → `Organization` + `WebSite` on the home page, |
| 7 | +`partials/schema-article.html` → `Article` + `BreadcrumbList` on `articles` and |
| 8 | +`podcast` pages). `robots.txt` (`layouts/robots.txt`, enabled by |
| 9 | +`enableRobotsTXT`) advertises the sitemap. Render-blocking third-party CSS is |
| 10 | +replaced by a purged, self-hosted Tailwind build and self-hosted Inter. |
| 11 | + |
| 12 | +Two load-bearing constraints shaped the asset half of this decision. |
| 13 | + |
| 14 | +**The deploy builds run bare `hugo`.** Both `netlify.toml` and |
| 15 | +`.github/workflows/deploy.yml` build with `hugo --minify`, not `npm run build`. |
| 16 | +So a CSS step wired into npm would never run in CI. The purged stylesheet is |
| 17 | +therefore a **committed artifact** (`assets/css/tailwind.css`, ~35 KB), not a |
| 18 | +build-time output. It is regenerated by `npm run build:css` |
| 19 | +(`scripts/build-tailwind.mjs`): download the pinned Tailwind 2.2.19 source, build |
| 20 | +the whole site to a throwaway dir, and purge the source against that output |
| 21 | +(`purgecss.config.cjs`). Hugo then `minify | fingerprint`s the committed file at |
| 22 | +build time so the immutable `Cache-Control` in `netlify.toml` is safe. |
| 23 | + |
| 24 | +**Purging against built HTML misses classes that aren't in the committed output.** |
| 25 | +The extractor keeps Tailwind tokens (`:` `/` `.` survive), and runtime-toggled |
| 26 | +classes (Alpine `:class`, JS `classList`) are caught because they appear as |
| 27 | +literals in the HTML. The gap is classes injected from data that only exists at |
| 28 | +deploy time — the activity-dot colors written into `data/community_stats.json` by |
| 29 | +`.github/scripts/fetch-discourse-activity.js` (and the `deploy.yml` fallback). |
| 30 | +Those are pinned in the `purgecss.config.cjs` safelist. |
| 31 | + |
| 32 | +## Considered options |
| 33 | + |
| 34 | +- **Keep the CDN links** (full Tailwind ~2.9 MB + FontAwesome + Prism, all |
| 35 | + render-blocking; Inter via a `@import` in an inline `<style>`) — rejected. The |
| 36 | + unpurged Tailwind download alone dominates first paint, and an `@import` is the |
| 37 | + worst case for blocking when Inter ships in `assets/fonts/` already. |
| 38 | +- **PostCSS/Tailwind compiled in the Hugo build** (`css.PostCSS`) — rejected. It |
| 39 | + needs committed `node_modules` (tailwindcss + postcss) and a JIT migration off |
| 40 | + v2; the deploy builds don't run npm, so it would not execute in CI anyway. |
| 41 | +- **Regenerate via Tailwind v3/v4** — rejected. v3's palette and resets differ |
| 42 | + from the v2 classes the markup was authored against, risking silent visual |
| 43 | + drift. Purging the *exact* v2 file keeps retained rules byte-identical. |
| 44 | +- **Purge the committed v2 file against the built site; self-host Inter** — |
| 45 | + chosen. No visual change for used classes, ~99% smaller CSS, one third-party |
| 46 | + render-blocking origin removed, and the Google Fonts round-trip eliminated. |
| 47 | + |
| 48 | +## Consequences |
| 49 | + |
| 50 | +- **Adding a new Tailwind class requires `npm run build:css`** and committing the |
| 51 | + result. The failure mode is visible, not silent: `npm run dev` serves the same |
| 52 | + purged file, so a missing class shows up locally before it ships. FontAwesome |
| 53 | + and Prism stay on the CDN (FontAwesome's CSS has relative `../webfonts/` font |
| 54 | + refs that would break if naively rehosted), so a preconnect is kept for them. |
| 55 | +- **A new build-time data class must be added to the safelist.** Anything driven |
| 56 | + by `community_stats.json` (or future data) that isn't present in committed |
| 57 | + markup will be purged unless pinned. The status-color palette is already pinned; |
| 58 | + `bg-orange-500` in the `deploy.yml` fallback is a no-op because orange is not in |
| 59 | + the stock Tailwind v2 palette — it never rendered, on the CDN or after. |
| 60 | +- **Inter ships only weights 400 and 700** (the two files in `assets/fonts/`). |
| 61 | + Heavier display weights used by `.gotham-black` (900) synthesize from 700; this |
| 62 | + was visually verified on the hero as acceptable. Adding a weight means adding |
| 63 | + the font file and a matching `@font-face`. |
| 64 | +- **JSON-LD is gated by section.** `Article`/`BreadcrumbList` render for `articles` |
| 65 | + and `podcast`; a new content section that should carry article schema must be |
| 66 | + added to the guard in `baseof.html` and the breadcrumb branch in |
| 67 | + `schema-article.html`. |
0 commit comments