See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add meta description tag to layout

Layout now emits <meta name="description"> with a sensible default;
profile pages reuse ogDescription, about page passes its own.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+14 -14
+11 -12
SEO-TODO.md
··· 3 3 ## P0 — Critical 4 4 5 5 1. ~~**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. 6 - 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. 6 + 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. 7 7 3. **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. 8 8 9 9 ## P1 — High 10 10 11 - 4. **No `X-Robots-Tag` on `/health`, `/api`, `/oauth`** — add middleware in `start/kernel.ts` to set `X-Robots-Tag: noindex` on these routes. 12 - 5. **Profile avatars use `alt=""`** (`resources/views/pages/profile/show.edge:22`) — use `alt="@{handle} avatar"` for SEO/a11y. 13 - 6. **Weak heading hierarchy** — display name is a styled `<div>` (`profile/show.edge:28`), no `<h1>`. Replace with semantic heading. 14 - 7. **No JSON-LD structured data** — add `Person` schema on profile pages, `ItemList` for post lists, to unlock rich results in Google SERPs. 11 + 4. **Profile avatars use `alt=""`** (`resources/views/pages/profile/show.edge:22`) — use `alt="@{handle} avatar"` for SEO/a11y. 12 + 5. **Weak heading hierarchy** — display name is a styled `<div>` (`profile/show.edge:28`), no `<h1>`. Replace with semantic heading. 13 + 6. **No JSON-LD structured data** — add `Person` schema on profile pages, `ItemList` for post lists, to unlock rich results in Google SERPs. 15 14 16 15 ## P2 — Medium 17 16 18 - 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"). 19 - 9. **Missing `hreflang`** — only `lang="en"` on `<html>` (`layout.edge:2`). Low impact but easy win: add `<link rel="hreflang" hreflang="en" href="{{ canonicalUrl }}" />`. 20 - 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`. 21 - 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. 22 - 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. 17 + 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"). 18 + 8. **Missing `hreflang`** — only `lang="en"` on `<html>` (`layout.edge:2`). Low impact but easy win: add `<link rel="hreflang" hreflang="en" href="{{ canonicalUrl }}" />`. 19 + 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`. 20 + 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. 21 + 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. 23 22 24 23 ## Already solid 25 24 ··· 33 32 34 33 ## Suggested implementation order 35 34 36 - 1. ~~robots.txt~~ + X-Robots-Tag middleware (~5 min) 37 - 2. Meta descriptions (~10 min) 35 + 1. ~~robots.txt~~ (~5 min) — **done** 36 + 2. ~~Meta descriptions~~ (~10 min) — **done** 38 37 3. Avatar alt text (~5 min) 39 38 4. JSON-LD Person + ItemList (~30 min) 40 39 5. Heading hierarchy fix (~10 min)
+1
resources/views/components/layout.edge
··· 13 13 <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/fill/style.css" /> 14 14 <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/bold/style.css" /> 15 15 <meta name="csrf-token" content="{{ csrfToken }}"> 16 + <meta name="description" content="{{ $props.get('description', 'See the most popular posts from any Bluesky account.') }}" /> 16 17 @if(posthogApiKey) 17 18 <meta name="posthog-api-key" content="{{ posthogApiKey }}"> 18 19 @if(auth && auth.isAuthenticated)
+1 -1
resources/views/pages/about.edge
··· 1 - @component('components/layout') 1 + @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." }) 2 2 @slot('title') 3 3 About — favs.blue 4 4 @endslot
+1 -1
resources/views/pages/profile/show.edge
··· 1 - @component('components/layout', { canonicalUrl }) 1 + @component('components/layout', { canonicalUrl, description: ogDescription }) 2 2 @slot('title') 3 3 Top {{ kind === 'likes' ? 'liked' : 'reposted' }} posts of {{ '@' + handle }} — favs.blue 4 4 @endslot