Skip to content

feat(seo): add meta description, canonical link and sitemap - #3

Merged
ClaraVnk merged 1 commit into
mainfrom
feat/seo-metadata
Jul 20, 2026
Merged

feat(seo): add meta description, canonical link and sitemap#3
ClaraVnk merged 1 commit into
mainfrom
feat/seo-metadata

Conversation

@ClaraVnk

Copy link
Copy Markdown
Contributor

Contexte

The landing page shipped without a <meta name="description"> or a canonical URL, and the site exposed no sitemap β€” the three gaps an SEO audit flags first.

Ghostbit is a zero-knowledge paste service, so "add SEO metadata" is not uniformly desirable: a paste URL is a capability. The ID grants access to the ciphertext and the decryption key lives in the fragment. Getting one into a search index distributes that capability to everyone. The indexing surface is therefore deliberately limited to the landing page.

Changements

  • meta description and <link rel="canonical"> on the landing page, built through the existing abs_url() helper so they respect BASE_URL behind a TLS-terminating proxy.
  • New {% block seo %} in base.html, separate from block meta. Needed because paste.html overrides block meta wholesale β€” without the split, every template opting out of indexing would have to restate all og:/twitter: tags.
  • noindex, nofollow on paste.html, raw.html and error.html, with no canonical on those pages (a noindex page advertising itself as canonical is contradictory).
  • New GET /sitemap.xml listing a single entry (/) β€” it does not walk storage.
  • Sitemap: directive in robots.txt, emitted per request as an absolute URL so self-hosters on any domain get a correct one with no configuration.

No Disallow was added for paste paths, deliberately: a path blocked in robots.txt prevents crawlers from ever reading the noindex directive on it, which is the opposite of the intent.

Tests

  • 4 new tests in tests/test_api.py: sitemap directive in robots.txt, single-entry sitemap with correct content type, paste page carries noindex and no canonical, landing page carries description + canonical and no noindex.
  • Full suite: 114 passed locally (110 pre-existing).
  • ruff check and ruff format --check clean on app/ tests/ cli/.
  • Rendered /robots.txt and /sitemap.xml inspected by hand.

Not exercised in a browser: the change is <head> tags plus two text endpoints, with no visual surface.

Risques

Low. No behavior change for existing routes; the only new route is /sitemap.xml, registered ahead of the /{paste_id} catch-all (and unreachable by it anyway β€” the ID pattern rejects dots).

The one judgement call worth review is the noindex on paste pages. It is a privacy tightening rather than a pure SEO fix, and it does not affect link-preview unfurls: Slack/iMessage bots fetch pages directly instead of going through a search index.

The landing page shipped without a meta description or canonical URL, and
the site exposed no sitemap.

Indexing directives move into their own `seo` block in base.html so pages
can opt out of the index without restating every og:/twitter: tag, which
`block meta` would otherwise force.

Paste URLs are capability URLs β€” the ID grants access to the ciphertext and
the key lives in the fragment. Indexing one distributes that capability, so
paste, raw and error pages are noindex with no canonical, and the sitemap
lists only the landing page rather than walking storage. Link-preview bots
read pages directly, so unfurls are unaffected.

No Disallow is added for paste paths: a blocked path stops crawlers from
ever reading the noindex directive.
@ClaraVnk

Copy link
Copy Markdown
Contributor Author

Merging with Test (redis) red, deliberately and on an informed basis.

That job is failing on untouched main for the same reason β€” baseline run 29729210049, 110 passed, 1 error. The error is a fixture teardown (RuntimeError: Event loop is closed), not a test. This PR adds 4 tests, all passing: 114 passed, 1 error.

Lint and Test (sqlite) are green.

The pre-existing failure is tracked separately in #4, where the first diagnosis was falsified and the investigation is still open. It is unrelated to this change, which touches no production async code.

@ClaraVnk
ClaraVnk merged commit 8b6dc63 into main Jul 20, 2026
2 of 3 checks passed
@ClaraVnk
ClaraVnk deleted the feat/seo-metadata branch July 20, 2026 09:03
ClaraVnk added a commit that referenced this pull request Jul 20, 2026
Nothing has shipped since v1.5.0: production still serves that build, so the
SEO, privacy, performance and accessibility work merged in #3 and #7-#10 is
present on main and absent from ghostbit.dev.

Pushes to main only publish the `edge` and `sha-*` image tags. The semver and
`latest` tags β€” which is what a pinned deployment follows β€” are produced only
by a v*.*.* git tag, and the last one predates all of it.

Server changes in this release:
  - meta description, canonical link, /sitemap.xml, noindex on paste pages
  - star count fetched server-side; no third-party call from the browser and
    connect-src back to 'self'
  - landing page 928 KB -> 336 KB, LCP 6.0s -> 3.0s; paste page 515 KB -> 307 KB
  - form controls labelled, <main> landmark, light-mode contrast fixed
  - dead static assets removed, including a source map misnamed .sha256

Note the CLI has no changes since v1.5.0, yet this publishes ghostbit-cli 1.6.0
to PyPI with identical code: release.yml reads its version from
cli/pyproject.toml, so a server-only release cannot be cut without it. Worth
decoupling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant