docs-kit 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72b8c93e9a6c2896bb31bd9ba8aaaf8cf5692424ab84d92ca8e5817d1b85291c
4
- data.tar.gz: bfc8a48ab90cbb0d62083f864fd8f188a645070b86bf4af1ff63c9a302160e99
3
+ metadata.gz: df9e88e060d79197aaf8f9247000b31f5da3568bbf7099f18f59bc216b11e877
4
+ data.tar.gz: 1fde31a5d995087e06b62f2b58983e73acfc12309d6958db6e913f538894edbf
5
5
  SHA512:
6
- metadata.gz: 6d6846fbbe1169e4513a2061b298a15ae65281da4eacaf3486d34392ceb52ecfcff7542df8eb28c2f8d5ff2c3b443c0e75a8371019b524293a51e76fbd964e80
7
- data.tar.gz: ee32039817d7e32af7b6a97e2abb2bed4ff5142974fe8e2c43f40949755343e8d6c96781d82603b61fa4794f92f3c24ef78832d33be0288c32c39308dfea1a64
6
+ metadata.gz: 8c8de40c603fa96e5fbee8aa377816894566c3b4f40c632e51d1a1055b875fe48b3349ef9cc833f0bada6b85d3504cfdc433ef27c9cdb9fff16ce30e81f0aa16
7
+ data.tar.gz: 7230b76a2e9d68d98061c92b2002ad59a2ca9a4609df75d8de2c68f4d911c78938188e1b87d1ced4c3ad7ff52ea4e9e6105fe8b0d2f43c27da826b77297f82fb
data/CHANGELOG.md CHANGED
@@ -2,20 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Fixed
6
+
7
+ - **SEO `og:image` 404.** The og:image tag pointed at the raw config path
8
+ (`https://site/og/og.png`), which isn't a served URL — Propshaft serves the
9
+ digested asset under `/assets`. A relative `og_image` is now resolved through
10
+ the site's asset pipeline (`image_url`) to the digested `/assets/og/og-<digest>.png`
11
+ URL, and the image is treated as **site content**: the gem ships none,
12
+ `c.seo.og_image` defaults to **nil** (unset → no og:image tag, never a 404), and
13
+ the `docs_kit:og` task writes into the *site's* `app/assets/images/`. Added a
14
+ booted-app integration test in the dogfood site
15
+ (`docs/test/integration/seo_meta_tags_test.rb`) that asserts og:image resolves
16
+ to a `/assets` URL that actually returns 200 — the coverage isolated component
17
+ specs can't provide.
18
+
5
19
  ### Added
6
20
 
7
21
  - **SEO + social sharing.** Every page now emits a complete SEO `<head>` —
8
22
  meta description, Open Graph, Twitter Card, canonical, favicon, robots, and
9
23
  theme-color — via the new `DocsUI::MetaTags` component, driven entirely by a
10
24
  new `c.seo` config block (`DocsKit::SeoConfig`). Pages carry an authorable
11
- `description "..."` (falling back to the page's `#lead`). docs-kit ships a
12
- neutral 1200×630 default OG image, and installs a `docs_kit:og` rake task that
13
- screenshots a site's OWN landing page into `app/assets/images/og/` (host-side
14
- headless browser; never a gem runtime dependency). A guard spec keeps the
15
- shipped default present. Backwards-compatible: a site that sets no `c.seo`
16
- renders a valid minimal card and its `<head>` is a strict superset of before.
17
- The install generator documents `c.seo` and installs the task + default image;
18
- `docs-kit new` reminds the owner to run it. (#48)
25
+ `description "..."` (falling back to the page's `#lead`). The social-share image
26
+ is site content (not shipped by the gem): the `docs_kit:og` rake task screenshots
27
+ a site's OWN landing page into its `app/assets/images/og/` (host-side headless
28
+ browser; never a gem runtime dependency), and `c.seo.og_image` points at it.
29
+ Backwards-compatible: a site that sets no `c.seo` renders a valid minimal card
30
+ and its `<head>` is a strict superset of before. The install generator documents
31
+ `c.seo` and installs the task; `docs-kit new` reminds the owner to run it. (#48)
19
32
  - Release tooling matching the sibling gems (daisyui/phlex-reactive/pgbus): a
20
33
  `rake release[X.Y.Z]` task (version bump → lockfile update → build-verify →
21
34
  commit → push → GitHub Release; `pre`/`force` supported, `main`-only, clean-tree
data/README.md CHANGED
@@ -180,9 +180,9 @@ nothing still gets a valid minimal card, so this is fully backwards-compatible.
180
180
  ```ruby
181
181
  # config/initializers/docs_kit.rb
182
182
  c.seo.description = "What these docs cover, in one sentence."
183
- c.seo.og_image = "og/og.png" # generated by `bin/rails docs_kit:og`
183
+ c.seo.og_image = "og/og.png" # a path in YOUR app/assets/images/
184
184
  c.seo.twitter_site = "@your_handle"
185
- c.seo.site_url = "https://docs.example.com" # absolutizes og:image + canonical
185
+ c.seo.site_url = "https://docs.example.com" # your canonical base URL
186
186
  # c.seo.robots = "noindex, nofollow" # keep a staging/private site out of search
187
187
  # c.seo.theme_color = "#0f172a" # tints mobile browser chrome
188
188
  ```
@@ -199,19 +199,26 @@ class Views::Docs::Pages::Installation < DocsUI::Page
199
199
  end
200
200
  ```
201
201
 
202
- **The social-share image.** docs-kit ships a neutral 1200×630 default so
203
- `og:image` is never broken. Generate one from your **own** landing page it
204
- screenshots `/` into `app/assets/images/og/{og,twitter,square}.png` whenever the
205
- page changes materially:
202
+ **The social-share image is your content, not the gem's.** docs-kit ships no OG
203
+ image your landing page isn't docs-kit's to render. Until you set `c.seo.og_image`,
204
+ **no `og:image` tag is emitted** (a valid card, never a broken-image 404). Generate
205
+ one from your **own** landing page it screenshots `/` into
206
+ `app/assets/images/og/{og,twitter,square}.png` — then point `og_image` at it:
206
207
 
207
208
  ```bash
208
209
  bin/rails docs_kit:og # needs a headless browser: shot-scraper or chromium/chrome
209
210
  ```
210
211
 
211
- It's a documented, manual routine (never run at deploy time), so a machine without
212
- a browser is never blocked. A guard spec keeps the shipped default present. Set
213
- `DOCS_KIT_OG_URL` to shoot a deployed URL instead of booting locally, or
214
- `DOCS_KIT_SHOT` to force a specific browser CLI.
212
+ `og_image` is a **logical asset path in your pipeline** (`"og/og.png"`), resolved
213
+ through `image_url` to the digested `/assets/og/og-<digest>.png` URL Propshaft
214
+ serves never the raw path, which 404s. So the image must be **precompiled**
215
+ (your Dockerfile's `assets:precompile` handles this at deploy); a configured
216
+ `og_image` that isn't in the pipeline fails loudly at deploy, not silently in
217
+ production. An absolute URL (`"https://cdn.example.com/card.png"`) is used verbatim.
218
+
219
+ `docs_kit:og` is a documented, manual routine (never run at deploy time), so a
220
+ machine without a browser is never blocked. Set `DOCS_KIT_OG_URL` to shoot a
221
+ deployed URL instead of booting locally, or `DOCS_KIT_SHOT` to force a browser CLI.
215
222
 
216
223
  ### Custom nav (advanced)
217
224
 
@@ -11,12 +11,15 @@ module DocsUI
11
11
  #
12
12
  # All free text (title, description, brand) is emitted as normal Phlex
13
13
  # attribute values, so Phlex escapes it — config free text is never trusted
14
- # markup. og:image/og:url/canonical are absolutized against config.seo.site_url
15
- # when set, else the request base URL; with neither, og:image degrades to its
16
- # raw path and canonical/og:url are omitted (guarded like Shell#csp_nonce, so
17
- # an isolated render or a request-less static build never raises).
14
+ # markup. The og:image is SITE content (nil by default → no og:image tag): a
15
+ # relative og_image is resolved through the SITE'S asset pipeline (image_url) to
16
+ # the DIGESTED /assets URL Propshaft serves NOT the raw config path, which
17
+ # 404s; an absolute URL passes through. canonical/og:url come from
18
+ # config.seo.site_url, else the request URL; both are omitted off a request
19
+ # (guarded like Shell#csp_nonce, so an isolated render never raises).
18
20
  class MetaTags < Phlex::HTML
19
21
  include Phlex::Rails::Helpers::Request
22
+ include Phlex::Rails::Helpers::ImageURL
20
23
 
21
24
  # title: the page title (nil on a page that sets none, e.g. the home
22
25
  # page) — combined with config.title_suffix for og:title.
@@ -108,17 +111,37 @@ module DocsUI
108
111
  meta(name: "theme-color", content: seo.theme_color)
109
112
  end
110
113
 
111
- # The og:image as an absolute-when-possible URL: an already-absolute
112
- # og_image is returned as-is; a relative path is joined onto the resolved
113
- # base (config.seo.site_url or the request base); with no base it degrades to
114
- # the raw path so og:image is never empty.
114
+ # The og:image as an absolute URL a crawler can fetch, or nil to emit NO
115
+ # og:image (a valid card without an image never a 404). nil when og_image is
116
+ # unset (the default). An already-absolute og_image passes through. A relative
117
+ # path is a logical asset in the SITE'S pipeline, resolved through image_url to
118
+ # the DIGESTED, host-qualified /assets URL Propshaft actually serves
119
+ # (https://host/assets/og/og-<digest>.png) — never the raw config path, which
120
+ # 404s. Off a request (an isolated render / static build) there is no asset
121
+ # pipeline to resolve a relative path, so we emit nothing rather than a
122
+ # guessed-and-wrong URL; a real app always renders with a view context.
115
123
  def og_image_url
116
124
  image = seo.og_image
117
125
  return if image.to_s.empty?
118
126
  return image if absolute?(image)
119
127
 
120
- base = base_url
121
- base ? "#{base}/#{image.delete_prefix('/')}" : image
128
+ resolve_asset(image)
129
+ end
130
+
131
+ # Resolve a logical asset path to its served (digested, host-qualified) URL via
132
+ # the Rails asset helper. nil when there's no view context (image_url delegates
133
+ # to view_context, which raises without one — the same seam Shell#csp_nonce
134
+ # guards), so an isolated render emits no og:image rather than raising.
135
+ #
136
+ # A configured-but-unresolvable og_image (asset missing / not precompiled)
137
+ # raises the pipeline's MissingAssetError — intended, NOT rescued: a broken
138
+ # og_image is a real misconfiguration that must surface at deploy time
139
+ # (assets:precompile runs before the app serves), not ship silently-broken
140
+ # social cards. A site with no card image leaves og_image nil (see #og_image_url).
141
+ def resolve_asset(path)
142
+ return unless view_context
143
+
144
+ image_url(path)
122
145
  end
123
146
 
124
147
  # The canonical/og:url for this page. From config.seo.site_url when set (its
@@ -132,28 +155,8 @@ module DocsUI
132
155
  request.original_url
133
156
  end
134
157
 
135
- # The base URL for absolutizing a relative og:image: config.seo.site_url's
136
- # origin, else the request base URL, else nil. site_url may include a path
137
- # (for canonical); strip it to an origin so the image path joins cleanly.
138
- def base_url
139
- return origin_of(seo.site_url) if seo.site_url
140
- return unless request?
141
-
142
- request.base_url
143
- end
144
-
145
158
  def absolute?(url) = url.to_s.match?(%r{\Ahttps?://}i)
146
159
 
147
- # Just the scheme+host(+port) of a URL, dropping any path — so joining an
148
- # image path onto it never doubles a path segment.
149
- def origin_of(url)
150
- uri = URI.parse(url)
151
- port = uri.port && uri.default_port != uri.port ? ":#{uri.port}" : ""
152
- "#{uri.scheme}://#{uri.host}#{port}"
153
- rescue URI::InvalidURIError
154
- url
155
- end
156
-
157
160
  # True only when there's a live Rails view context AND a request on it — the
158
161
  # phlex-rails #request helper delegates to view_context, which raises without
159
162
  # one. Mirrors Shell#csp_nonce's guard so an isolated Phlex render (the specs,
@@ -22,18 +22,20 @@ module DocsKit
22
22
  # (DocsUI::Page derives a per-page value from #lead when a page sets none).
23
23
  attr_accessor :description
24
24
 
25
- # The social-share image, as an asset path (resolved via the Rails asset
26
- # helper) or an absolute URL og:image / twitter:image. Defaults to the
27
- # neutral image docs-kit ships at app/assets/images/og/og.png so og:image is
28
- # never broken; a site regenerates its own with `bin/rails docs_kit:og`.
25
+ # The social-share image og:image / twitter:image. Either a logical asset
26
+ # path in the SITE'S OWN pipeline (e.g. "og/og.png", resolved through
27
+ # image_url to the digested /assets URL Propshaft serves) or an absolute URL.
28
+ # The image is SITE content, not shipped by the gem — nil by default, so a
29
+ # site with no card image emits NO og:image (a valid card, never a 404).
30
+ # Generate one into app/assets/images/ with `bin/rails docs_kit:og`, then set
31
+ # this to its path.
29
32
  attr_accessor :og_image
30
33
 
31
34
  # The og:type. "website" for a docs site; a page could override to "article".
32
35
  attr_accessor :og_type
33
36
 
34
37
  # The twitter:card style. "summary_large_image" renders a full-width banner
35
- # (the shipped og:image is 1200×630, sized for it); "summary" is the small
36
- # square card.
38
+ # (size your og:image 1200×630 for it); "summary" is the small square card.
37
39
  attr_accessor :twitter_card
38
40
 
39
41
  # The site's @handle → twitter:site, and the content author's → twitter:creator.
@@ -64,7 +66,7 @@ module DocsKit
64
66
 
65
67
  def initialize
66
68
  @description = nil
67
- @og_image = "og/og.png"
69
+ @og_image = nil
68
70
  @og_type = "website"
69
71
  @twitter_card = "summary_large_image"
70
72
  @twitter_site = nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DocsKit
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.2"
5
5
  end
@@ -168,18 +168,14 @@ module DocsKit
168
168
  create_file "app/assets/builds/.keep", ""
169
169
  end
170
170
 
171
- # The SEO social-share image routine: the `docs_kit:og` rake task (gem-owned
172
- # wiring, refreshed on every run so a site gets task fixes) plus the neutral
173
- # default OG image (site content `copy_file` skips it if the site already
174
- # generated/customized its own, so `bin/rails docs_kit:og` output is never
175
- # clobbered). c.seo.og_image defaults to "og/og.png" so a site's og:image is
176
- # valid immediately, before it runs the task.
171
+ # Install the `docs_kit:og` rake task gem-owned wiring, refreshed on every
172
+ # run so a site picks up task fixes. It does NOT ship an OG image: the
173
+ # social-share image is SITE content, generated into the site's OWN
174
+ # app/assets/images/ by `bin/rails docs_kit:og`. Until a site runs it (and
175
+ # sets c.seo.og_image), no og:image tag is emitted — a valid card, never a
176
+ # 404 for an image the gem can't provide.
177
177
  def create_og_task
178
178
  template "docs_kit_og.rake", "lib/tasks/docs_kit_og.rake"
179
- copy_file(
180
- File.expand_path("../../../../app/assets/images/og/og.png", __dir__),
181
- "app/assets/images/og/og.png"
182
- )
183
179
  end
184
180
 
185
181
  def wire_assets_and_package_json
@@ -36,19 +36,21 @@ Rails.application.config.to_prepare do
36
36
  # that sets none still gets a valid minimal card. Per-page descriptions come
37
37
  # from each page's `description "..."` (falling back to its lead paragraph).
38
38
  # c.seo.description = "What these docs cover, in one sentence."
39
- # c.seo.og_image = "og/og.png" # generated by `bin/rails docs_kit:og`
40
39
  # c.seo.twitter_card = "summary_large_image" # default; "summary" = small card
41
40
  # c.seo.twitter_site = "@your_handle"
42
41
  # c.seo.twitter_creator = "@author_handle"
43
- # c.seo.site_url = "https://docs.example.com" # absolutizes og:image/canonical off a request
42
+ # c.seo.site_url = "https://docs.example.com" # your canonical base URL
44
43
  # c.seo.locale = "en_US"
45
44
  # c.seo.robots = "noindex, nofollow" # keep a staging/private site out of search
46
45
  # c.seo.favicon = "/favicon.ico"
47
46
  # c.seo.theme_color = "#0f172a" # tints mobile browser chrome
48
47
  #
49
- # Regenerate the social-share image from your OWN landing page (screenshots
50
- # "/" into app/assets/images/og/) whenever it changes materially:
48
+ # The social-share image is YOUR content, not shipped by the gem. Generate one
49
+ # from your landing page (screenshots "/" into app/assets/images/og/) and point
50
+ # og_image at it — it's resolved through YOUR asset pipeline to the digested
51
+ # /assets URL. Until you set it, no og:image is emitted (a valid card, no 404).
51
52
  # bin/rails docs_kit:og
53
+ # c.seo.og_image = "og/og.png" # a path in app/assets/images/
52
54
 
53
55
  # Code blocks use one Rouge theme by default. To keep them readable in BOTH
54
56
  # light and dark daisyUI themes, set a light base + a dark override — the dark
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docs-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -164,8 +164,6 @@ files:
164
164
  - CHANGELOG.md
165
165
  - LICENSE.txt
166
166
  - README.md
167
- - app/assets/images/og/og.png
168
- - app/assets/images/og/og.svg
169
167
  - app/components/docs_ui/brand_mark.rb
170
168
  - app/components/docs_ui/callout.rb
171
169
  - app/components/docs_ui/code.rb
Binary file
@@ -1 +0,0 @@
1
- <svg height="630" viewBox="0 0 1200 630" width="1200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" x1="0" x2="1" y1="0" y2="1"><stop offset="0" stop-color="#0f172a"/><stop offset="1" stop-color="#1e293b"/></linearGradient><linearGradient id="b" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#38bdf8"/><stop offset="1" stop-color="#818cf8"/></linearGradient><path d="m0 0h1200v630h-1200z" fill="url(#a)"/><path d="m0 0h1200v8h-1200z" fill="url(#b)"/><path d="m80 470h240m-240 40h180m-180 40h220" fill="none" opacity=".5" stroke="#334155" stroke-width="2"/><g transform="translate(80 150)"><rect fill="url(#b)" height="96" rx="20" width="96"/><path d="m28 32h40m-40 16h40m-40 16h28" stroke="#0f172a" stroke-linecap="round" stroke-width="7"/></g><g font-family="Helvetica, Arial, sans-serif"><text fill="#f8fafc" font-size="88" font-weight="700" x="80" y="330">Documentation</text><text fill="#94a3b8" font-size="34" font-weight="400" x="82" y="392">Built with docs-kit</text></g></svg>