···33## P0 — Critical
44551. ~~**No `robots.txt` or `sitemap.xml`**~~ — **DONE.** `public/robots.txt` added; dynamic sitemap at `/sitemap.xml` (index) + `/sitemaps/:n.xml` (chunks, 25k profiles each). Only backfilled, non-deleted profiles included.
66-2. **No `<meta name="description">`** — only OG descriptions exist. Add to `resources/views/components/layout.edge` (or per page: `landing.edge`, `profile/show.edge`, `about.edge`). OG description is set at `app/controllers/profile_controller.ts:364` but not mirrored to the standard meta tag.
66+2. ~~**No `<meta name="description">`**~~ — **DONE.** Added a `description` prop to `layout.edge` with a sensible default; profile pages reuse `ogDescription`; about page passes its own.
773. **Duplicate-content risk on handle variants** — `/profile/@alice` 301s to `/profile/alice/likes` (`profile_controller.ts:205-212`), but URL-encoded `%40` variants may still get crawled. Consider a route-level constraint or middleware.
8899## P1 — High
10101111-4. **No `X-Robots-Tag` on `/health`, `/api`, `/oauth`** — add middleware in `start/kernel.ts` to set `X-Robots-Tag: noindex` on these routes.
1212-5. **Profile avatars use `alt=""`** (`resources/views/pages/profile/show.edge:22`) — use `alt="@{handle} avatar"` for SEO/a11y.
1313-6. **Weak heading hierarchy** — display name is a styled `<div>` (`profile/show.edge:28`), no `<h1>`. Replace with semantic heading.
1414-7. **No JSON-LD structured data** — add `Person` schema on profile pages, `ItemList` for post lists, to unlock rich results in Google SERPs.
1111+4. **Profile avatars use `alt=""`** (`resources/views/pages/profile/show.edge:22`) — use `alt="@{handle} avatar"` for SEO/a11y.
1212+5. **Weak heading hierarchy** — display name is a styled `<div>` (`profile/show.edge:28`), no `<h1>`. Replace with semantic heading.
1313+6. **No JSON-LD structured data** — add `Person` schema on profile pages, `ItemList` for post lists, to unlock rich results in Google SERPs.
15141615## P2 — Medium
17161818-8. **Landing page title is generic** — uses the layout default (`resources/views/components/layout.edge:30`). Add a `@slot('title')` with a CTA-oriented title in `landing.edge` (e.g. "favs.blue — Find the best posts from any Bluesky account").
1919-9. **Missing `hreflang`** — only `lang="en"` on `<html>` (`layout.edge:2`). Low impact but easy win: add `<link rel="hreflang" hreflang="en" href="{{ canonicalUrl }}" />`.
2020-10. **No preload/dns-prefetch** for fonts or `cdn.bsky.app` — add to `layout.edge` head. Affects Core Web Vitals (a ranking signal). Preconnect exists but no `preload`/`dns-prefetch`.
2121-11. **Inconsistent `Cache-Control`** — landing/about have none; profile pages set `max-age=60` (`profile_controller.ts:367`); OG images set `max-age=86400` (`og_image_controller.ts:10,18`). Document and align.
2222-12. **No per-post OG images** — route `/og/profile/:handle.png` exists (`start/routes.ts:102`) but no `/og/post/:did/:rkey.png`; shares of specific posts miss rich previews.
1717+7. **Landing page title is generic** — uses the layout default (`resources/views/components/layout.edge:30`). Add a `@slot('title')` with a CTA-oriented title in `landing.edge` (e.g. "favs.blue — Find the best posts from any Bluesky account").
1818+8. **Missing `hreflang`** — only `lang="en"` on `<html>` (`layout.edge:2`). Low impact but easy win: add `<link rel="hreflang" hreflang="en" href="{{ canonicalUrl }}" />`.
1919+9. **No preload/dns-prefetch** for fonts or `cdn.bsky.app` — add to `layout.edge` head. Affects Core Web Vitals (a ranking signal). Preconnect exists but no `preload`/`dns-prefetch`.
2020+10. **Inconsistent `Cache-Control`** — landing/about have none; profile pages set `max-age=60` (`profile_controller.ts:367`); OG images set `max-age=86400` (`og_image_controller.ts:10,18`). Document and align.
2121+11. **No per-post OG images** — route `/og/profile/:handle.png` exists (`start/routes.ts:102`) but no `/og/post/:did/:rkey.png`; shares of specific posts miss rich previews.
23222423## Already solid
2524···33323433## Suggested implementation order
35343636-1. ~~robots.txt~~ + X-Robots-Tag middleware (~5 min)
3737-2. Meta descriptions (~10 min)
3535+1. ~~robots.txt~~ (~5 min) — **done**
3636+2. ~~Meta descriptions~~ (~10 min) — **done**
38373. Avatar alt text (~5 min)
39384. JSON-LD Person + ItemList (~30 min)
40395. Heading hierarchy fix (~10 min)
+1
resources/views/components/layout.edge
···1313 <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/fill/style.css" />
1414 <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/bold/style.css" />
1515 <meta name="csrf-token" content="{{ csrfToken }}">
1616+ <meta name="description" content="{{ $props.get('description', 'See the most popular posts from any Bluesky account.') }}" />
1617 @if(posthogApiKey)
1718 <meta name="posthog-api-key" content="{{ posthogApiKey }}">
1819 @if(auth && auth.isAuthenticated)
+1-1
resources/views/pages/about.edge
···11-@component('components/layout')
11+@component('components/layout', { description: "About favs.blue — a favstar.fm-style site for Bluesky. See any account's most popular posts, updated in near real-time." })
22 @slot('title')
33 About — favs.blue
44 @endslot