main
rb 57 lines 2.02 KB
Raw
1 # frozen_string_literal: true
2
3 # Serves the Open Graph share-card PNGs referenced by the OG/Twitter meta tags.
4 #
5 # Cards are cached by content hash (OgImageService never runs on the hot path
6 # when a cached card exists) and answered with a long-lived, public, ETag'd
7 # response so crawlers and CDNs only fetch bytes once. Private repositories are
8 # never rendered — the endpoint 404s so no preview can leak.
9 class OgImagesController < ApplicationController
10 CACHE_TTL = 7.days
11 FALLBACK_IMAGE = "favicon/web-app-manifest-512x512.png"
12 FALLBACK_CACHE_TTL = 5.minutes
13
14 # GET /og/:username/:repository.png
15 def show
16 repository = public_repository
17 return head :not_found unless repository
18
19 serve_card("#{OgImageService::TEMPLATE_VERSION}-#{repository.og_version}") do
20 OgImageService.render(repository: repository)
21 end
22 end
23
24 # GET /og.png — the generic sitewide card (default og:image).
25 def default
26 serve_card("#{OgImageService::TEMPLATE_VERSION}-default") do
27 OgImageService.render_default
28 end
29 end
30
31 private
32
33 def public_repository
34 owner = User.find_by(username: params[:username])
35 return nil unless owner
36
37 repository = owner.repositories.find_by(name: params[:repository])
38 return nil if repository.nil? || repository.is_private
39
40 repository
41 end
42
43 # Conditional-GET + cache wrapper. Yields the PNG bytes only on a cache miss;
44 # on any rendering failure (e.g. libvips missing) falls back to a static image
45 # with a short TTL so the failure self-heals.
46 def serve_card(version)
47 return unless stale?(etag: version, public: true)
48
49 png = Rails.cache.fetch([ "og", version ], expires_in: CACHE_TTL) { yield }
50 expires_in CACHE_TTL, public: true
51 send_data png, type: "image/png", disposition: "inline"
52 rescue StandardError => e
53 Rails.logger.warn("[OG] falling back to static card: #{e.class}: #{e.message}")
54 expires_in FALLBACK_CACHE_TTL, public: true
55 send_file Rails.public_path.join(FALLBACK_IMAGE), type: "image/png", disposition: "inline"
56 end
57 end