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 +4 -4
- data/CHANGELOG.md +21 -8
- data/README.md +17 -10
- data/app/components/docs_ui/meta_tags.rb +33 -30
- data/lib/docs_kit/seo_config.rb +9 -7
- data/lib/docs_kit/version.rb +1 -1
- data/lib/generators/docs_kit/install/install_generator.rb +6 -10
- data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +6 -4
- metadata +1 -3
- data/app/assets/images/og/og.png +0 -0
- data/app/assets/images/og/og.svg +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df9e88e060d79197aaf8f9247000b31f5da3568bbf7099f18f59bc216b11e877
|
|
4
|
+
data.tar.gz: 1fde31a5d995087e06b62f2b58983e73acfc12309d6958db6e913f538894edbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`).
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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" #
|
|
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" #
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
page
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
`
|
|
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
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# an
|
|
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
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# the
|
|
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
|
-
|
|
121
|
-
|
|
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,
|
data/lib/docs_kit/seo_config.rb
CHANGED
|
@@ -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
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
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
|
-
# (
|
|
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 =
|
|
69
|
+
@og_image = nil
|
|
68
70
|
@og_type = "website"
|
|
69
71
|
@twitter_card = "summary_large_image"
|
|
70
72
|
@twitter_site = nil
|
data/lib/docs_kit/version.rb
CHANGED
|
@@ -168,18 +168,14 @@ module DocsKit
|
|
|
168
168
|
create_file "app/assets/builds/.keep", ""
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
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" #
|
|
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
|
-
#
|
|
50
|
-
# "/" into app/assets/images/og/)
|
|
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.
|
|
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
|
data/app/assets/images/og/og.png
DELETED
|
Binary file
|
data/app/assets/images/og/og.svg
DELETED
|
@@ -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>
|