Static Site Optimizer
Overview
Takes a folder of plain static sites and, for each one, makes the site load
entirely from its own origin: every stylesheet, script, font and image
pulled from a CDN is downloaded into assets/ and the reference rewritten to
the local copy. On top of that it applies a fixed list of mechanical hygiene
fixes, runs Lighthouse, and writes a per-site AUDIT.md plus an aggregated
dashboard Artifact.
The whole pass is driven by scripts/optimize.js (Node, no runtime
dependencies). Only --compress-images and the Lighthouse step reach for
external tooling via npx.
Why this matters — GDPR exposure, one source of truth, one DNS resolution,
supply-chain risk, visitors behind blockers — is set out in
reference/why-self-hosting.md. Read it before arguing with a user who wants to
keep a CDN reference, and cite it in AUDIT.md when a tracker or embed stays.
This skill is deliberately narrow. Left to a generic "improve this site"
instruction, a capable agent will rewrite contact forms, guess the site's real
domain, edit business content, convert image formats and fix application bugs
it happens to notice — all plausible, all outside what a hygiene pass should
touch without a human's sign-off. This skill replaces that free-for-all with
the fixed procedure and fix list below.
When NOT to use
- Sites with a build pipeline (React/Vue/bundlers) — this targets plain
HTML/CSS/JS served as-is, with no build step to invert.
- When the user wants design/UX changes, content changes, or bug fixes beyond
the list below — those stay report-only by design.
The ONLY automatic fixes in scope
All applied by node scripts/optimize.js <site> --fix:
Removing external references (the point of the skill)
<link rel="stylesheet">, <script src>, <img src|srcset>,
<source>, <video poster>, <link rel="preload|icon|manifest"> pointing
at another origin → downloaded under assets/{css,js,img,fonts,media}/ and
rewritten to the local path.
- A downloaded stylesheet is followed one level deeper: its
@import and
url() refs (webfonts, background images) are fetched too and rewritten
relative to the stylesheet's new location. This is what turns a Google Fonts
<link> or @import into local .woff2 files.
font-display: swap is added to downloaded @font-face blocks that lack it.
integrity and crossorigin are dropped from a tag once its asset is local
(the SRI hash described the remote copy).
Mechanical hygiene
- Missing
<meta charset>, missing <meta viewport>
- Missing
lang on <html> — see Language below. Applied only when the
language is known, never guessed.
- Missing
width/height on <img> (read from the real image file);
loading="lazy" on every image but the first two of each page
- Missing
rel="noopener noreferrer" on <a target="_blank">
- Plain-text emails and phone numbers →
mailto: / tel: links
- Missing
robots.txt / sitemap.xml / README.md / llms.txt (scaffolded,
with the sitemap and llms.txt listing every HTML page found)
- Missing
favicon.ico → a generated placeholder letter tile, not real branding
- With
--compress-images: JPEG/PNG over 150KB re-encoded via npx sharp-cli,
same format and dimensions, kept only if at least 10% smaller
Never auto-fixed — report only
- Trackers and analytics (Google Analytics, GTM, Meta Pixel, Hotjar…):
reported with their host. Removing them changes what the site does.
- Third-party embeds (Maps, YouTube, Vimeo iframes): must stay external to
work. Reported with a suggestion to use a click-to-load facade.
<a href> outbound links and <form action>: never rewritten, ever.
- Stale
preconnect/dns-prefetch hints left over after self-hosting
- Meta description content, alt-text quality, colour contrast, heading
hierarchy, CSS/JS minification, ARIA/focus management, application bugs,
links to Office documents
Nothing else is ever auto-fixed. Do not additionally: rewrite forms or
their endpoints, invent or assume the site's real domain, edit business content
(emails, phone numbers, addresses, prices), convert images between formats, fix
JS bugs you notice, or restructure HTML/CSS beyond the list above. Anything
broken or improvable that is not on the fix list goes into AUDIT.md under
"Da rivedere manualmente" as a suggestion — never as something you fixed.
Language
lang is the one fix whose value cannot be read off the file being fixed, so the
optimizer resolves it in this order and says which rung it landed on:
--lang <code> passed explicitly — always wins.
- A language the site declares about itself on any of its pages (
<html lang>,
og:locale, content-language, a single hreflang). Applied automatically:
this is evidence from the site, not a guess — the same rule that governs domain
names.
- A stopword analysis of the page text. Reported as a suggestion and applied
to nothing.
missing-lang stays open with autoFixed: false.
node scripts/optimize.js <site> --detect-lang returns
{ lang, source, confidence, evidence } without touching anything, so you can ask
the user before doing any work. A wrong lang makes a screen reader mispronounce
every word, which is worse than no attribute — hence rung 3 never writes.
Procedure
Discover sites
node <skill-dir>/scripts/optimize.js --discover <target-dir>
Returns { "sites": string[] } (a folder counts as a site if it has an
index.html in its root). If the array is empty, tell the user no static
site was found under that folder and stop.
Settle the language, once, before doing any work. For each site run:
node <skill-dir>/scripts/optimize.js <site> --detect-lang
This is offline and instant. Then:
confidence: "high" for every site — the sites declare their own language.
Do not ask; carry on and let the optimizer use it.
- Otherwise — ask the user with AskUserQuestion, offering the detected
language as the recommended first option, e.g. "Italiano (
it) —
rilevato dal testo delle pagine" alongside a couple of plausible
alternatives. One question covers all sites when they agree; ask per site
when the detections differ. Pass the answer as --lang <code> in step 3.
lang: null (nothing declared, too little text) — ask the same way, with no
recommended option, and say the detection came back empty.
Never invent a language, and never leave the decision to the per-site agents:
they see one site each and would answer inconsistently.
Optimize each site in parallel. For every site path returned, dispatch a
general-purpose Agent (foreground, no worktree — each site is already an
isolated git repo) with this prompt, substituting {siteDir}, {skillDir}
and {langFlag} ( --lang <code> from step 2, or empty when the site
declares its own language):
Optimize the static website at {siteDir}. You are executing a fixed
procedure — do not use your own judgment to go beyond it, no matter how
clearly beneficial an extra change looks.
- Run
node {skillDir}/scripts/optimize.js {siteDir} and read the JSON findings.
- Run
node {skillDir}/scripts/optimize.js {siteDir} --fix --compress-images{langFlag}
to apply the safe automatic fixes. This downloads external assets into
{siteDir}/assets/ and rewrites the references to them; it needs network
access. If a download fails the original URL is left intact and reported —
do not hand-edit it yourself.
- Start a local static server (
npx --yes serve {siteDir} -l 4173), then run
npx --yes lighthouse http://localhost:4173 --output=json --output-path=stdout --chrome-flags="--headless" --only-categories=performance,accessibility,best-practices,seo.
Stop the server afterwards. Do not skip this step or estimate the scores
yourself if the server is slow to start or Lighthouse takes a while — wait
for it, or retry; a slow local server is normal, not a reason to fabricate
scores or omit them.
- Write
{siteDir}/AUDIT.md with exactly four sections:
"Punteggi Lighthouse" (the 4 category scores);
"Riferimenti esterni" (what was self-hosted, and what is still external —
trackers, embeds, failed downloads — each with its host);
"Fix applicati automaticamente" (every finding with autoFixed: true);
"Da rivedere manualmente" (every finding with autoFixed: false, plus your
own read of the pages for alt-text quality, meta description, colour
contrast and heading hierarchy — the script does not judge these).
Anything broken, outdated or improvable that is NOT one of the fixes this
skill performs goes in the last section as a suggestion, never as done.
- Do NOT modify any
<a href> outbound link, any <form action>, or any
third-party embed. Do NOT edit business content (emails, phone numbers,
addresses, prices). Do NOT assume or write in a domain name that isn't
already present verbatim in the site's own files. Do NOT convert any image
to another format. Do NOT run any git command. Do NOT commit.
- Return exactly this JSON as your final message and nothing else — no prose
before or after:
{ "site": "{siteDir}", "scores": { "performance": N, "accessibility": N, "best-practices": N, "seo": N }, "autoFixed": N, "selfHostedFiles": N, "externalRefsRemaining": N, "openFindings": [...] }
Aggregate. Once every per-site agent has returned, build an Artifact HTML
dashboard (load the artifact-design skill first) comparing Lighthouse
scores across sites, self-hosted file counts, external references still
remaining, and open findings per site.
Report to the user. Per site: scores, fixes applied, how many external
references were eliminated and which remain (naming the hosts). Call out
explicitly, by site name, any generated favicon.ico — it is a coloured
letter tile, not branding, and is the one auto-fix a human must replace
rather than merely review. Remind them nothing was committed: they should
review git status / git diff in each site's repo first.
Constraints
- Only resource-loading tags are in scope for self-hosting.
<a href>,
<form action> and <iframe src> are never rewritten — changing those
changes what the page does, not how it loads.
- Never edit business content: emails, phone numbers, addresses, prices, form
endpoints. A form pointing at a broken endpoint, a wrong email or a similar
real bug is an
AUDIT.md finding — never an edit, even when the correct fix
seems obvious from elsewhere on the site.
- Never assume, guess or write in a domain name. Only use one already present
verbatim in the site's own files (prefer relative paths in
sitemap.xml over
inventing an absolute URL).
- Never convert an image to another format, and never resize one.
--compress-images
re-encodes at the same format and dimensions or leaves the file alone.
- Never run
git commit, git push, or any other git command.
- A tracker is reported, never removed and never self-hosted.
Reference
reference/why-self-hosting.md — the rationale: privacy/GDPR, one source of
truth, one DNS resolution, supply-chain security, blocked visitors
reference/checks.md — every finding code, what triggers it, and whether it
is auto-fixed
reference/self-hosting.md — how the asset store, naming and path rewriting
work, and what to do when a download fails
1---2name: static-site-optimizer3description: Use when the user wants to optimize, audit or clean up one or more static HTML/CSS/JS websites in a folder — above all to cut every external dependency (CDN fonts, CDN scripts and stylesheets, remote images) by self-hosting it, plus mechanical performance/SEO/accessibility hygiene (meta tags, image dimensions, lazy loading, robots.txt, sitemap.xml, favicon, README, llms.txt). Applies ONLY the fixed list of safe automatic fixes below; everything else is report-only.4---56# Static Site Optimizer78## Overview910Takes a folder of plain static sites and, for each one, makes the site load11**entirely from its own origin**: every stylesheet, script, font and image12pulled from a CDN is downloaded into `assets/` and the reference rewritten to13the local copy. On top of that it applies a fixed list of mechanical hygiene14fixes, runs Lighthouse, and writes a per-site `AUDIT.md` plus an aggregated15dashboard Artifact.1617The whole pass is driven by `scripts/optimize.js` (Node, no runtime18dependencies). Only `--compress-images` and the Lighthouse step reach for19external tooling via `npx`.2021Why this matters — GDPR exposure, one source of truth, one DNS resolution,22supply-chain risk, visitors behind blockers — is set out in23`reference/why-self-hosting.md`. Read it before arguing with a user who wants to24keep a CDN reference, and cite it in `AUDIT.md` when a tracker or embed stays.2526**This skill is deliberately narrow.** Left to a generic "improve this site"27instruction, a capable agent will rewrite contact forms, guess the site's real28domain, edit business content, convert image formats and fix application bugs29it happens to notice — all plausible, all outside what a hygiene pass should30touch without a human's sign-off. This skill replaces that free-for-all with31the fixed procedure and fix list below.3233## When NOT to use3435- Sites with a build pipeline (React/Vue/bundlers) — this targets plain36 HTML/CSS/JS served as-is, with no build step to invert.37- When the user wants design/UX changes, content changes, or bug fixes beyond38 the list below — those stay report-only by design.3940## The ONLY automatic fixes in scope4142All applied by `node scripts/optimize.js <site> --fix`:4344**Removing external references** (the point of the skill)4546- `<link rel="stylesheet">`, `<script src>`, `<img src|srcset>`,47 `<source>`, `<video poster>`, `<link rel="preload|icon|manifest">` pointing48 at another origin → downloaded under `assets/{css,js,img,fonts,media}/` and49 rewritten to the local path.50- A downloaded stylesheet is followed one level deeper: its `@import` and51 `url()` refs (webfonts, background images) are fetched too and rewritten52 relative to the stylesheet's new location. This is what turns a Google Fonts53 `<link>` or `@import` into local `.woff2` files.54- `font-display: swap` is added to downloaded `@font-face` blocks that lack it.55- `integrity` and `crossorigin` are dropped from a tag once its asset is local56 (the SRI hash described the remote copy).5758**Mechanical hygiene**5960- Missing `<meta charset>`, missing `<meta viewport>`61- Missing `lang` on `<html>` — see **Language** below. Applied only when the62 language is known, never guessed.63- Missing `width`/`height` on `<img>` (read from the real image file);64 `loading="lazy"` on every image but the first two of each page65- Missing `rel="noopener noreferrer"` on `<a target="_blank">`66- Plain-text emails and phone numbers → `mailto:` / `tel:` links67- Missing `robots.txt` / `sitemap.xml` / `README.md` / `llms.txt` (scaffolded,68 with the sitemap and llms.txt listing every HTML page found)69- Missing `favicon.ico` → a generated placeholder letter tile, **not** real branding70- With `--compress-images`: JPEG/PNG over 150KB re-encoded via `npx sharp-cli`,71 same format and dimensions, kept only if at least 10% smaller7273## Never auto-fixed — report only7475- **Trackers and analytics** (Google Analytics, GTM, Meta Pixel, Hotjar…):76 reported with their host. Removing them changes what the site does.77- **Third-party embeds** (Maps, YouTube, Vimeo iframes): must stay external to78 work. Reported with a suggestion to use a click-to-load facade.79- **`<a href>` outbound links and `<form action>`**: never rewritten, ever.80- Stale `preconnect`/`dns-prefetch` hints left over after self-hosting81- Meta description content, alt-text quality, colour contrast, heading82 hierarchy, CSS/JS minification, ARIA/focus management, application bugs,83 links to Office documents8485**Nothing else is ever auto-fixed.** Do not additionally: rewrite forms or86their endpoints, invent or assume the site's real domain, edit business content87(emails, phone numbers, addresses, prices), convert images between formats, fix88JS bugs you notice, or restructure HTML/CSS beyond the list above. Anything89broken or improvable that is not on the fix list goes into `AUDIT.md` under90"Da rivedere manualmente" as a suggestion — never as something you fixed.9192## Language9394`lang` is the one fix whose value cannot be read off the file being fixed, so the95optimizer resolves it in this order and says which rung it landed on:96971. `--lang <code>` passed explicitly — always wins.982. A language the site **declares about itself** on any of its pages (`<html lang>`,99 `og:locale`, `content-language`, a single `hreflang`). Applied automatically:100 this is evidence from the site, not a guess — the same rule that governs domain101 names.1023. A **stopword analysis of the page text**. Reported as a *suggestion* and applied103 to nothing. `missing-lang` stays open with `autoFixed: false`.104105`node scripts/optimize.js <site> --detect-lang` returns106`{ lang, source, confidence, evidence }` without touching anything, so you can ask107the user before doing any work. A wrong `lang` makes a screen reader mispronounce108every word, which is worse than no attribute — hence rung 3 never writes.109110## Procedure1111121. **Discover sites**113114 ```115 node <skill-dir>/scripts/optimize.js --discover <target-dir>116 ```117118 Returns `{ "sites": string[] }` (a folder counts as a site if it has an119 `index.html` in its root). If the array is empty, tell the user no static120 site was found under that folder and stop.1211222. **Settle the language, once, before doing any work.** For each site run:123124 ```125 node <skill-dir>/scripts/optimize.js <site> --detect-lang126 ```127128 This is offline and instant. Then:129130 - `confidence: "high"` for every site — the sites declare their own language.131 Do not ask; carry on and let the optimizer use it.132 - Otherwise — ask the user with AskUserQuestion, **offering the detected133 language as the recommended first option**, e.g. "Italiano (`it`) —134 rilevato dal testo delle pagine" alongside a couple of plausible135 alternatives. One question covers all sites when they agree; ask per site136 when the detections differ. Pass the answer as `--lang <code>` in step 3.137 - `lang: null` (nothing declared, too little text) — ask the same way, with no138 recommended option, and say the detection came back empty.139140 Never invent a language, and never leave the decision to the per-site agents:141 they see one site each and would answer inconsistently.1421433. **Optimize each site in parallel.** For every site path returned, dispatch a144 `general-purpose` Agent (foreground, no worktree — each site is already an145 isolated git repo) with this prompt, substituting `{siteDir}`, `{skillDir}`146 and `{langFlag}` (` --lang <code>` from step 2, or empty when the site147 declares its own language):148149 > Optimize the static website at `{siteDir}`. You are executing a fixed150 > procedure — do not use your own judgment to go beyond it, no matter how151 > clearly beneficial an extra change looks.152 > 1. Run `node {skillDir}/scripts/optimize.js {siteDir}` and read the JSON findings.153 > 2. Run `node {skillDir}/scripts/optimize.js {siteDir} --fix --compress-images{langFlag}`154 > to apply the safe automatic fixes. This downloads external assets into155 > `{siteDir}/assets/` and rewrites the references to them; it needs network156 > access. If a download fails the original URL is left intact and reported —157 > do not hand-edit it yourself.158 > 3. Start a local static server (`npx --yes serve {siteDir} -l 4173`), then run159 > `npx --yes lighthouse http://localhost:4173 --output=json --output-path=stdout --chrome-flags="--headless" --only-categories=performance,accessibility,best-practices,seo`.160 > Stop the server afterwards. Do not skip this step or estimate the scores161 > yourself if the server is slow to start or Lighthouse takes a while — wait162 > for it, or retry; a slow local server is normal, not a reason to fabricate163 > scores or omit them.164 > 4. Write `{siteDir}/AUDIT.md` with exactly four sections:165 > "Punteggi Lighthouse" (the 4 category scores);166 > "Riferimenti esterni" (what was self-hosted, and what is still external —167 > trackers, embeds, failed downloads — each with its host);168 > "Fix applicati automaticamente" (every finding with `autoFixed: true`);169 > "Da rivedere manualmente" (every finding with `autoFixed: false`, plus your170 > own read of the pages for alt-text quality, meta description, colour171 > contrast and heading hierarchy — the script does not judge these).172 > Anything broken, outdated or improvable that is NOT one of the fixes this173 > skill performs goes in the last section as a suggestion, never as done.174 > 5. Do NOT modify any `<a href>` outbound link, any `<form action>`, or any175 > third-party embed. Do NOT edit business content (emails, phone numbers,176 > addresses, prices). Do NOT assume or write in a domain name that isn't177 > already present verbatim in the site's own files. Do NOT convert any image178 > to another format. Do NOT run any `git` command. Do NOT commit.179 > 6. Return exactly this JSON as your final message and nothing else — no prose180 > before or after:181 > `{ "site": "{siteDir}", "scores": { "performance": N, "accessibility": N, "best-practices": N, "seo": N }, "autoFixed": N, "selfHostedFiles": N, "externalRefsRemaining": N, "openFindings": [...] }`1821834. **Aggregate.** Once every per-site agent has returned, build an Artifact HTML184 dashboard (load the `artifact-design` skill first) comparing Lighthouse185 scores across sites, self-hosted file counts, external references still186 remaining, and open findings per site.1871885. **Report to the user.** Per site: scores, fixes applied, how many external189 references were eliminated and which remain (naming the hosts). Call out190 explicitly, by site name, any generated `favicon.ico` — it is a coloured191 letter tile, not branding, and is the one auto-fix a human must replace192 rather than merely review. Remind them nothing was committed: they should193 review `git status` / `git diff` in each site's repo first.194195## Constraints196197- Only resource-loading tags are in scope for self-hosting. `<a href>`,198 `<form action>` and `<iframe src>` are never rewritten — changing those199 changes what the page does, not how it loads.200- Never edit business content: emails, phone numbers, addresses, prices, form201 endpoints. A form pointing at a broken endpoint, a wrong email or a similar202 real bug is an `AUDIT.md` finding — never an edit, even when the correct fix203 seems obvious from elsewhere on the site.204- Never assume, guess or write in a domain name. Only use one already present205 verbatim in the site's own files (prefer relative paths in `sitemap.xml` over206 inventing an absolute URL).207- Never convert an image to another format, and never resize one. `--compress-images`208 re-encodes at the same format and dimensions or leaves the file alone.209- Never run `git commit`, `git push`, or any other git command.210- A tracker is reported, never removed and never self-hosted.211212## Reference213214- `reference/why-self-hosting.md` — the rationale: privacy/GDPR, one source of215 truth, one DNS resolution, supply-chain security, blocked visitors216- `reference/checks.md` — every finding code, what triggers it, and whether it217 is auto-fixed218- `reference/self-hosting.md` — how the asset store, naming and path rewriting219 work, and what to do when a download fails