Files
fofos-admin/skills/fofos-lighthouse/SKILL.md
T
pastilhasandClaude Opus 5 74c6776c75 a desktop audit was not a desktop audit
`formFactor: 'desktop'` changes which thresholds a metric is scored against and
NOTHING else. Throttling and the user agent keep their defaults, which are mobile
— so every desktop run was a desktop-sized window on a 4x-slowed CPU over slow 4G,
announced as a moto-g.

It read 86 where the same page reads 100. That gap was reported repeatedly as
network latency to the host. It was not latency; it was this. Measured after the
fix: desktop 100, LCP 0.4s against 2.2s.

Nine more from the same review:

  runtimeError was never checked, so a page that failed to load reported
  "nothing failing" — the most reassuring possible output for a site that is down

  --json exited 0 before the gate, so the documented non-zero exit was true for a
  person reading the table and false for the only caller that could act on it

  scoreDisplayMode 'error' was filtered out while still nulling its category, so
  a category could read 0 with an empty findings list

  a missing metric serialised as 0, which a script reads as a perfect measurement

  `source` is a string on some audits and an object on others; String() on the
  object printed [object Object]

  the watchdog's surrender was permanent — the health branch sat below the check,
  so once it stopped trying it also stopped noticing, and a server fixed by hand
  left it asleep until the platform restarted

  a failed restart leaves no process at all, because startServer deletes before
  it starts. status null then reads as "somebody stopped it", which the watchdog
  leaves alone — one bad restart would have turned it off and taken the server
  with it, silently

  the skill claimed the panel uses this engine. Nothing but the CLI does

  and gave `bun run build:site` with no cwd, which only works from the website
  checkout — api/static-site.ts documents that and the skill did not

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 18:42:45 +01:00

6.8 KiB
Raw Blame History

name, title, description, version
name title description version
fofos-lighthouse Measuring the fofos site Use when working on the fofos website's performance, accessibility, SEO or Core Web Vitals — running an audit, reading what it found, and the traps that make a number mean something other than it appears to. 1

bin/lighthouse.ts runs Lighthouse against a URL in a real Chrome and prints what failed, with the resources each finding names. The engine is api/lighthouse.ts, which is where the panel will read from when it grows a screen for this — today the CLI is the only caller.

Running it

# The platform checkout is named differently per machine — officer-dev here, officerdev on fofos —
# so the glob finds it either way rather than the doc being right on one box and wrong on the other.
bun "$(ls -d ~/officer*/platform/plugins/fofos-admin | head -1)/bin/lighthouse.ts" <url>
flag effect
(none) mobile and desktop, in that order — about 20 seconds
--mobile / --desktop one form factor
--json the whole result on stdout; progress goes to stderr, so the pipe stays clean
--quiet no progress narration

Exit code is 0 only when every category on every form factor is 100. Non-zero otherwise, so it can gate something without being parsed.

Measure the BUILT site

The dev server at localhost:3011 renders from the database on every request. The public site is static files under dist/, served by nginx behind a proxy, with different caching, different compression and a trailing-slash redirect. They are not the same page and their numbers do not agree.

So: build first, then audit the public URL. The build has to run in the website checkout and with the site's own environment — see api/static-site.ts, which documents both:

cd ~/officerdev/data/*/FofosWebsite/website && bun run build:site

A score from the dev server is not wrong, it is about something nobody visits.

Reading the output

═══ MOBILE  https://web.fofosfluffybastards.com/gallery/aldi/
     ✓ performance 99    ✓ accessibility 100    ✓ best-practices 100    ✓ seo 100

    First Contentful Paint     1.0 s
    Largest Contentful Paint   2.2 s
    Total Blocking Time        0 ms
    Cumulative Layout Shift    0
    Speed Index                1.0 s

    [  0] Improve image delivery — Est savings of 47 KiB
           …/ALDI_LIDL_0000_1.png/960.avif  47 KiB wasted, 57 KiB served, li.w-[70%] > … > img.h-full

The bracketed number is that audit's own score out of 100. The indented line is the resource — a URL, what it cost, and the CSS path to the element. That last part is what makes a finding fixable: it tells you which block emitted it, which tells you which component to look at.

Only scored audits appear. informative, manual and notApplicable are filtered out — they never had a score, and listing them turns a report into noise nobody reads.

The five metrics, and which one your change moved

Performance is computed from these and nothing else. Insights are diagnostics; they do not score.

  • Total Blocking Time — JavaScript. Mobile throttles the CPU 4×, so a library parsed at load shows up here and nowhere else. Moving 56 KB of GLightbox to load-on-intent took this from 130 ms to 0.
  • Largest Contentful Paint — usually the biggest image above the fold. Wants three things together: preloaded, fetchpriority="high", and NOT loading="lazy". Missing any one is a separate failing audit.
  • First Contentful Paint — the fonts, mostly. font-display: block holds text back until a face arrives, so the preloads in Layout.tsx are what keep this honest. They went missing once and cost 200 ms on every page but Home.
  • Cumulative Layout Shift — anything sized after it paints. Should stay 0.
  • Speed Index — how fast the page fills in. Follows from the other four.

Traps

A desktop audit is desktop in four ways, and formFactor is only one of them. Setting formFactor: 'desktop' changes which thresholds a metric is scored against and NOTHING else — the throttling and the user agent keep their defaults, which are mobile. A run configured that way is a desktop-sized window crawling on an emulated phone, and it reads about 86 where the same page reads 100. api/lighthouse.ts sets screenEmulation, throttling and emulatedUserAgent together for this reason; if you write your own runner, set all four or the number is about nothing.

A number from another machine includes that machine's network. Compare like with like: two runs from the same place, before and after a change. An absolute score from somewhere else is a different question — though check the paragraph above first, because that explains far more of a gap than latency usually does.

Run-to-run variance is real — a point or two either way on the same build. Chase a finding, not a number. If a score moves and no finding changed, nothing happened.

sizes is not a decoration. It tells the browser which rung to fetch before layout exists, so a sizes that overstates a column downloads a file two rungs too big and looks perfect. columnSizes in components/PageSection.tsx computes it from the same spacing the band lays itself out with, precisely so a padding change in the Layout panel cannot silently break it.

A preload must name the format and the rung the <picture> will actually pick. Given href alone it fetches one file and the <img> then chooses another — two downloads of the largest thing on the page, which is worse than no preload. imagesrcset, imagesizes and type all travel together or none of them do.

What this has already caught on this site

Worth knowing, because each was invisible until something measured:

  • a sizes string claiming a 624px column that renders 546 — the old constant assumed the band had no padding, so every cover fetched a rung too big
  • the font preloads gone entirely, while the comment explaining them survived in Layout.tsx and build-fonts.ts still pointed at a file that had been moved
  • an LCP image left loading="lazy" with no preload, because a rename from collection-cover to subject-cover missed the line that hunts for it — a string comparison the type checker cannot see

The pattern: all three were renames or settings changes that left something behind, and none produced an error. That is what this is for.

The browser

Looked for in three places before anything is downloaded — CHROME_PATH, chrome-launcher's own search, then Chrome-family binaries by name (Brave counts; it is Chromium underneath). Only if all three miss does @puppeteer/browsers fetch one into ~/.cache/officer/browsers, about 150 MB, once.

If an audit fails with something about a browser, that chain is where to look — chromePath() in api/lighthouse.ts narrates which one it chose on every run.