main
md 3.27 KB

Social link unfurls & SEO for public repos

How pasted siGit links become preview cards + backlinks, and how to verify it.

What's emitted

Every page renders, in the initial server HTML (no JS required):

  • <title>, <meta name="description">, <link rel="canonical">
  • Open Graph: og:type, og:site_name, og:title, og:description, og:url, og:image (+ og:image:type/width/height/alt), og:locale
  • Twitter: twitter:card=summary_large_image, title, description, image, alt
  • JSON-LD: Organization + WebSite sitewide; SoftwareSourceCode on public repo pages.

Public repo pages (repositories#show, #tree, #cicd, blobs#show, repositories#model) override the defaults with the repo's name, owner, and description via app/views/shared/_repository_social.html.erb + SeoHelper.

Auth / transactional pages (/auth*, /settings, /billing, /new) call noindex! in their controller, emitting robots: noindex, nofollow.

OG share-card images

GET /og/:owner/:repo.png → a 1200×630 PNG (repo name, owner, wrapped description, primary-language dot, star count, siGit wordmark). GET /og.png is the generic sitewide card and the default og:image.

  • Rendered by OgImageService (SVG template → PNG via libvips; the prod image installs libvips, librsvg2-2, and fonts-dejavu-core).
  • Cached by content hash (Repository#og_version: tip commit + name + description + stars). Served public, long max-age, with an ETag so repeat fetches are 304s. Never generated on the hot path when cached.
  • Privacy: private repos 404 here (no card, even for the owner) and are excluded from the sitemap. A logged-out crawler hitting a private repo URL gets the static 404 with zero metadata.
  • Fallback: if rasterization is unavailable, the endpoint serves the static brand icon with a short TTL so it self-heals — it never 500s.

robots.txt / sitemap.xml

  • public/robots.txt allows content, disallows /auth /settings /billing /new /api/ /*/raw/, and points at the sitemap.
  • GET /sitemap.xml (SitemapsController) lists marketing/legal pages, every public repo, and the profiles that own one. Private repos and private-only users are excluded. Cached 6h.

Verify locally

# Meta tags are in the initial HTML (simulate a crawler):
curl -s -A "Twitterbot" http://localhost:3000/<owner>/<repo> | grep -iE 'og:|twitter:|canonical|application/ld'

# Private repo leaks nothing and 404s:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/<owner>/<private-repo>   # 404

# OG image (needs libvips locally; otherwise returns the static fallback PNG):
curl -sI http://localhost:3000/og/<owner>/<repo>.png    # 200, Content-Type: image/png, ETag, Cache-Control: public
curl -s http://localhost:3000/robots.txt
curl -s http://localhost:3000/sitemap.xml | head

# Tests:
bin/rails test

External validators (run against a deployed public URL): X Card Validator, Facebook Sharing Debugger, LinkedIn Post Inspector, Google Rich Results Test.

Note: macOS dev hosts usually lack libvips, so the OG endpoint returns the static fallback locally — the card itself renders in CI/production. To preview the card design without libvips, dump the SVG (OgImageService.send(:repository_svg, repo)) and open it in a browser.