You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
perf(seo): defer GA + Tally off critical path; a11y labels on sliders
- Lazy-load GA gtag.js and the Tally widget on first interaction / idle
instead of at page load (src/lib/analytics/defer-third-party.ts). The
gtag shim stays in app.html so events queue in dataLayer; Tally binds
its data-tally-open buttons on load. Pulls ~155 KiB + Tally JS off the
critical path (targets mobile INP, the only failing Core Web Vital).
- Add aria-label to all five Range sliders (line thickness, opacity,
line-pair gap, text size, word gap) — fixes the Lighthouse "form
elements must have labels" / agent-accessibility audit.
Field data (CrUX): desktop CWV passes; mobile fails only on INP (269ms),
LCP/CLS already green — so the fonts work (M3) was dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/seo-plan.md
+47-35Lines changed: 47 additions & 35 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -116,14 +116,8 @@ Each of 19 example pages was a dead-end.
116
116
### H5 — Move partner blocks below primary content on example pages ✅ ALREADY FINE
117
117
Re-checked the actual `examples/[slug]/+page.svelte`: the partner banner is already the **last** element (after body copy, figure, and the "Open in Editor" CTA). The content agents' "card before content" claim conflated this with the **homepage mobile** layout, where the Preply card does sit above the editor — that's tracked under **M9**, not here. No change needed on example pages.
118
118
119
-
### H6 — Fix touch target sizes in alignment editor
120
-
Below 48×48px minimum:
121
-
- Tooltip "?" buttons: 16×16px
122
-
- Toolbar icon buttons: 36×36px
123
-
- Delete row buttons: ~16px
124
-
- Word token boxes: varies with word length
125
-
126
-
Add `min-h-[44px] min-w-[44px]` to `.token-view--clickable`. Apply padding-based tap expansion to all icon-only buttons.
The proposed fix is inappropriate for this tool. Word tokens (`.token-view--clickable`) **are the rendered diagram text**, sized by the user's per-line font setting (12–64px) and baseline-aligned. Forcing `min-h-[44px] min-w-[44px]` would break the alignment layout — that's the whole visualization. The `?` partner button (16px) was verified to tap reliably on a real device. Toolbar icon buttons are 36px (acceptable). No change — this is inherent to a typography/alignment editor.
127
121
128
122
### H7 — Remove Inter font from partner card component ❌ DROPPED (misdiagnosis)
129
123
The performance agent attributed the runtime Inter stylesheet to the partner card. **Wrong:** Inter is the *default visualization font* (`DEFAULT_FONT_FAMILY = 'Inter'` in `src/lib/api/align.ts`; source/target/gloss line defaults). It is loaded on demand via `googleFontStylesheetUrl()` when the live preview renders, and is core functionality — not a partner-card waste. No partner component contains a `<svelte:head>` font link. Removing it would break the default preview. Not actioned.
@@ -147,12 +141,8 @@ Convert headings to question form:
147
141
- "Word alignment vs interlinear translation" → "What is the difference between word alignment and interlinear translation?"
148
142
- "Great for language learners and teachers" → "How do language learners and teachers use word alignment?"
149
143
150
-
### M3 — Self-host Space Grotesk (or preload font files)
151
-
3 Google Fonts families require 2 external connections + 2-hop waterfall (HTML → Fonts CSS → font files).
152
-
153
-
Minimum: add `<link rel="preload" as="font" type="font/woff2" crossorigin>` for Space Grotesk 400 in `app.html`.
154
-
155
-
Better: self-host via `@fontsource/space-grotesk` (npm). Add `size-adjust`/`ascent-override`/`descent-override` fallback metrics to eliminate FOUT CLS.
144
+
### M3 — Self-host Space Grotesk (or preload font files) ❌ DROPPED (field data)
145
+
Fonts are render-blocking (lab shows ~750ms each), but they affect **FCP/LCP — which already PASS in the field** (CrUX: mobile LCP 2.1s green, desktop 1.5s; **CLS already 0**, so the FOUT-CLS rationale is moot). Self-hosting wouldn't flip any ranking verdict. Also: "Google Sans" is in fact a public Google Font now and loads fine (the visual agent's note was stale). Not worth the change. (Render-blocking Inter could be deferred cheaply if ever bundling other font work — cosmetic.)
156
146
157
147
### M4 — Add `height` attribute to example page LCP images ✅
158
148
All example images had `width="960"` but no `height`. Browser can't reserve space → CLS.
### M7 — Rename 5 weak example slugs (with 301 redirects) → MOVED to content task (do with C2)
173
+
Deferred to the content task "SEO расширение контента страниц примеров": renaming a slug changes the URL and also requires a 301 redirect **and** renaming the CDN preview asset (`{slug}.png` on DO Spaces + the `preview-dimensions.ts` key + local cache). Cheapest to do per-page while expanding that page's content (C2), before the URLs are indexed. Low SEO value on its own.
183
174
184
175
| Current | Problem | New |
185
176
|---|---|---|
@@ -196,19 +187,22 @@ OG image generator endpoint returns 200 with no noindex signal. Not in sitemap (
196
187
197
188
**Done:** added `'X-Robots-Tag': 'noindex'` to the `/api/og` response headers.
198
189
199
-
### M9 — Fix mobile above-fold layout
200
-
On 375px: affiliate card renders above editor inputs; live preview SVG requires 3 screen-heights of scrolling.
201
-
- Apply `order-last` to affiliate card on small screens
202
-
- Anchor preview SVG below line inputs in mobile stack order (or sticky mini-preview)
-**"Hover-only tooltip"** — wrong. `PartnerBannerShell.svelte` already has `onclick={toggleWhy}` + `aria-expanded` + `touch-manipulation`; the tooltip opens on tap. Confirmed tapping fine on a real device.
193
+
-**"Preview 3 screens down / card on 30% of viewport"** — overstated. The partner intro card is one compact banner in the header's intro zone; on mobile it sits between the intro text and the editor. Reordering it cleanly would need restructuring the header/sidebar (card and editor live in separate containers, so `order` can't swap them). Low value, non-trivial fix.
194
+
195
+
Owner judged mobile adaptivity acceptable for an inherently multi-panel editor. No change.
204
196
205
197
### M10 — Trim homepage meta description to ≤155 chars ✅
206
198
Current: 243 chars (truncated in SERPs, cuts off audience mention).
207
199
208
200
**Done:**`DEFAULT_DESCRIPTION` in `metadata.ts` is now 155 chars: `Free word-by-word translation visualizer. Stack lines, add gloss/IPA, draw connectors, then export PNG, SVG, or PDF. For learners, teachers, and linguists.`
209
201
210
-
### M11 — Lazy-load Tally.so feedback widget
211
-
Tally has 1-hour `max-age` (controlled by Tally). Replace static `<script async>` in `<head>` with demand-load triggered on "Send feedback" click via `window.Tally.openPopup()`.
**Done:** removed the eager `<script async>` for **both** Tally and GA gtag.js from `app.html`. The tiny gtag shim stays (calls queue in `dataLayer`); the gtag.js library + Tally widget now load on first user interaction or `requestIdleCallback`, whichever first (`src/lib/analytics/defer-third-party.ts`, wired in `+layout.svelte`). This pulls ~155 KiB (GA) + the Tally script off the critical path — the INP/TBT direction. Tally binds its `data-tally-open` buttons on load; gtag events flush from the queue. Verified the eager scripts are gone from initial HTML and the shim remains.
204
+
205
+
Note: "Google Tag Manager 155 KiB" in Lighthouse is just gtag.js served from the `googletagmanager.com` host — it IS the GA4 tag (`G-…`), not the separate GTM container product. One thing, kept, just deferred.
212
206
213
207
### M12 — Start link acquisition campaign
214
208
Domain ~2 months old, zero third-party backlinks (expected). First moves:
@@ -223,6 +217,26 @@ The 404 was bare "404 Not Found" — no branding, no navigation.
223
217
224
218
**Done:** added `src/routes/+error.svelte` — branded layout (status, friendly heading/message), CTA "Open Word Aligner" + links to examples and API docs, and `noindex`. Renders inside the existing layout (theme/fonts) for all error statuses (404 and others).
225
219
220
+
### M14 — Accessibility: label the range sliders ✅ (found via Lighthouse)
221
+
Lighthouse "agent accessibility" flagged the text-size slider as a form element without a label (also a normal a11y gap). **Done:** added `aria-label` to all five Flowbite `Range` sliders (line thickness, line opacity, line-pair gap, text size, word gap). Helps screen readers and the "AI agent accessibility" category — on-brand given the API/skill target AI agents.
222
+
223
+
---
224
+
225
+
## Core Web Vitals — field data (CrUX, 28-day) & the real INP lever
The only failing metric is **mobile INP (269ms, needs-improvement; poor is >500ms)**. LCP and CLS already pass everywhere — which is why M3 (fonts) was dropped and render-blocking CSS is low priority (they target already-passing metrics).
235
+
236
+
**Cheap INP-direction work done:** deferred GA + Tally off the critical path (M11), labelled sliders (M14). These reduce load-time main-thread contention and may help early-interaction INP — confirm in CrUX over ~28 days.
237
+
238
+
**Remaining real INP lever (NOT done — needs profiling):** the editor's own interaction cost — tapping a token triggers Svelte reactivity → link-graph recompute → SVG redraw — plus the heavy `nodes/2` app chunk (~167 KiB, ~129 KiB unused per Lighthouse) and 2 long main-thread tasks. Options: code-split heavy sub-components (export/font/color dialogs) via dynamic `import()`, and optimize the per-tap update (batch DOM writes, memoize). This is a measured mini-project; only pursue if mobile INP doesn't drop below 200ms after the cheap changes settle in CrUX. CWV is a minor ranking factor and desktop already passes, so this is UX-driven, not urgent.
239
+
226
240
---
227
241
228
242
## Low (backlog)
@@ -278,20 +292,18 @@ The 404 was bare "404 Not Found" — no branding, no navigation.
278
292
-[x] M6 — Author bio + Person JSON-LD on /about
279
293
-[x] H5 — already fine on example pages (partner banner is last); homepage mobile → M9
280
294
281
-
**Week 1** (~8–10 h)
282
-
-[ ] C2 — Expand first 5 example pages (Hebrew-Arabic, Japanese-Chinese-EN, Nahuatl, Lezgian, Lojban)
283
-
-[ ] M3 — Self-host Space Grotesk or add font preload
295
+
**Day 3** (perf/a11y) — ✅ DONE 2026-06-25
296
+
-[x] M11 — Lazy-load Tally (+ defer GA gtag.js) off the critical path
297
+
-[x] M14 — aria-label all Range sliders (a11y + AI-agent accessibility)
298
+
-[~] M3 — Fonts — DROPPED (target LCP/CLS already pass in field; Google Sans is public)
284
299
285
300
**Week 2–3**
286
301
-[x] H4 — Cross-navigation "Related examples" on all example pages
-[~] H6 — Touch targets — WONTFIX (tokens are user-sized diagram text; can't force 44px)
305
+
306
+
**Remaining (technical):**
307
+
-[ ] (optional, measured) Editor INP — code-split heavy sub-components + optimize per-tap update. Only if mobile INP stays >200ms in CrUX after the deferrals settle. Needs profiling.
0 commit comments