main
rb 124 lines 4.44 KB
Raw
1 # frozen_string_literal: true
2
3 # On-page SEO helpers. The layout renders `shared/_seo`, which reads these to
4 # emit the title, description, canonical, Open Graph, Twitter Card, and JSON-LD
5 # tags. Pages override the defaults with `content_for`:
6 #
7 # <% content_for :title, "Models" %>
8 # <% content_for :description, "Browse open-weights models on siGit." %>
9 # <% content_for :og_image, image_or_absolute_url %> # optional
10 # <% content_for :robots, "noindex, follow" %> # optional
11 #
12 # Everything degrades to a sensible sitewide default, so a page that sets
13 # nothing still gets valid, non-empty tags.
14 module SeoHelper
15 SITE_NAME = "siGit"
16 TITLE_SUFFIX = "siGit Code & Deploy"
17
18 DEFAULT_DESCRIPTION =
19 "siGit is Git hosting for the AI era. Version your repositories, publish " \
20 "open-weights models with full model cards and LFS-backed files, and hand " \
21 "the keys to your AI coding agent over the Agent Client Protocol."
22
23 # Sitewide default Open Graph / Twitter image: the generic 1200×630 card
24 # rendered by OgImagesController#default (falls back to a static icon if image
25 # rasterization is unavailable). Per-page repo cards override this.
26 DEFAULT_OG_IMAGE_PATH = "/og.png"
27
28 # All generated cards are 1200×630, the summary_large_image sweet spot.
29 OG_IMAGE_WIDTH = 1200
30 OG_IMAGE_HEIGHT = 630
31
32 # "<page> · siGit Code & Deploy", or just the suffix on the bare root.
33 def page_full_title
34 page = content_for(:title)
35 page.present? ? "#{page} · #{TITLE_SUFFIX}" : TITLE_SUFFIX
36 end
37
38 def meta_description
39 strip_tags(content_for(:description).presence || DEFAULT_DESCRIPTION).squish
40 end
41
42 # Canonical URL without query string or fragment, so tracking params and
43 # pagination don't fork duplicate URLs in the index.
44 def canonical_url
45 "#{request.base_url}#{request.path}"
46 end
47
48 def og_image_url
49 custom = content_for(:og_image)
50 return custom if custom.present? && custom.start_with?("http")
51
52 "#{request.base_url}#{custom.presence || DEFAULT_OG_IMAGE_PATH}"
53 end
54
55 # Controllers flag auth-gated / transactional pages with `noindex!`; those
56 # always win so private surfaces never enter a search index even if a stray
57 # link points at them. Otherwise honor a per-view override, then the default.
58 def meta_robots
59 return "noindex, nofollow" if @noindex
60 content_for(:robots).presence || "index, follow"
61 end
62
63 def og_type
64 content_for(:og_type).presence || "website"
65 end
66
67 # Path to a repository's generated Open Graph card. Used both as the page's
68 # og:image override and inside its SoftwareSourceCode JSON-LD.
69 def repository_og_image_path(repository)
70 og_image_path(repository.user.username, repository.name)
71 end
72
73 # SoftwareSourceCode structured data for a public repository page. Rendered
74 # into <script type="application/ld+json"> via content_for(:structured_data).
75 def repository_structured_data(repository)
76 owner = repository.user
77 url = repository_url(owner.username, repository.name)
78 data = {
79 "@context" => "https://schema.org",
80 "@type" => "SoftwareSourceCode",
81 "name" => repository.name,
82 "description" => meta_description,
83 "url" => url,
84 "codeRepository" => url,
85 "dateCreated" => repository.created_at.utc.iso8601,
86 "dateModified" => repository.updated_at.utc.iso8601,
87 "image" => "#{request.base_url}#{repository_og_image_path(repository)}",
88 "author" => {
89 "@type" => "Person",
90 "name" => owner.username,
91 "url" => user_profile_url(owner.username)
92 }
93 }
94 if (lang = repository.primary_language)
95 data["programmingLanguage"] = lang.first
96 end
97 tag.script(data.to_json.html_safe, type: "application/ld+json")
98 end
99
100 # Organization + WebSite, emitted on every page. Establishes the site entity
101 # for knowledge-panel / sitelinks eligibility.
102 def organization_json_ld
103 {
104 "@context" => "https://schema.org",
105 "@graph" => [
106 {
107 "@type" => "Organization",
108 "@id" => "#{request.base_url}/#organization",
109 "name" => SITE_NAME,
110 "url" => request.base_url,
111 "logo" => "#{request.base_url}#{DEFAULT_OG_IMAGE_PATH}",
112 "description" => DEFAULT_DESCRIPTION
113 },
114 {
115 "@type" => "WebSite",
116 "@id" => "#{request.base_url}/#website",
117 "name" => SITE_NAME,
118 "url" => request.base_url,
119 "publisher" => { "@id" => "#{request.base_url}/#organization" }
120 }
121 ]
122 }
123 end
124 end