docs-kit 1.0.0 → 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 +25 -0
- data/README.md +51 -0
- data/app/components/docs_ui/meta_tags.rb +166 -0
- data/app/components/docs_ui/page.rb +14 -1
- data/app/components/docs_ui/shell.rb +14 -4
- data/lib/docs_kit/configuration.rb +11 -0
- data/lib/docs_kit/og_generator.rb +167 -0
- data/lib/docs_kit/seo_config.rb +81 -0
- data/lib/docs_kit/templates/new_site.rb +3 -0
- data/lib/docs_kit/version.rb +1 -1
- data/lib/docs_kit.rb +7 -0
- data/lib/generators/docs_kit/install/install_generator.rb +10 -0
- data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +22 -0
- data/lib/generators/docs_kit/install/templates/docs_kit_og.rake +44 -0
- metadata +5 -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,8 +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
|
|
|
21
|
+
- **SEO + social sharing.** Every page now emits a complete SEO `<head>` —
|
|
22
|
+
meta description, Open Graph, Twitter Card, canonical, favicon, robots, and
|
|
23
|
+
theme-color — via the new `DocsUI::MetaTags` component, driven entirely by a
|
|
24
|
+
new `c.seo` config block (`DocsKit::SeoConfig`). Pages carry an authorable
|
|
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)
|
|
7
32
|
- Release tooling matching the sibling gems (daisyui/phlex-reactive/pgbus): a
|
|
8
33
|
`rake release[X.Y.Z]` task (version bump → lockfile update → build-verify →
|
|
9
34
|
commit → push → GitHub Release; `pre`/`force` supported, `main`-only, clean-tree
|
data/README.md
CHANGED
|
@@ -169,6 +169,57 @@ Any other `icon:` value is treated as a **lucide** name and rendered through
|
|
|
169
169
|
sets nothing has an unchanged topbar. The marks use `fill: currentColor`, so they
|
|
170
170
|
recolor with the active daisyUI theme like the rest of the chrome.
|
|
171
171
|
|
|
172
|
+
### SEO & social sharing
|
|
173
|
+
|
|
174
|
+
Every page ships a complete SEO `<head>` — a meta description, **Open Graph**,
|
|
175
|
+
**Twitter Card**, canonical, favicon, and theme-color — so a link shared to
|
|
176
|
+
Slack/Discord/X/LinkedIn renders a rich card instead of a bare URL. It's all
|
|
177
|
+
config-driven (`DocsUI::MetaTags` reads `DocsKit.configuration`); a site that sets
|
|
178
|
+
nothing still gets a valid minimal card, so this is fully backwards-compatible.
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# config/initializers/docs_kit.rb
|
|
182
|
+
c.seo.description = "What these docs cover, in one sentence."
|
|
183
|
+
c.seo.og_image = "og/og.png" # a path in YOUR app/assets/images/
|
|
184
|
+
c.seo.twitter_site = "@your_handle"
|
|
185
|
+
c.seo.site_url = "https://docs.example.com" # your canonical base URL
|
|
186
|
+
# c.seo.robots = "noindex, nofollow" # keep a staging/private site out of search
|
|
187
|
+
# c.seo.theme_color = "#0f172a" # tints mobile browser chrome
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Per-page descriptions.** A `DocsUI::Page` sets its own with `description "..."`;
|
|
191
|
+
when it doesn't, the description is derived from the page's `#lead`, so existing
|
|
192
|
+
pages get a sensible per-page description for free:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
class Views::Docs::Pages::Installation < DocsUI::Page
|
|
196
|
+
title "Installation"
|
|
197
|
+
description "Add the gem and render your first component." # optional
|
|
198
|
+
def lead = "..." # used as the description when none is set
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
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:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
bin/rails docs_kit:og # needs a headless browser: shot-scraper or chromium/chrome
|
|
210
|
+
```
|
|
211
|
+
|
|
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.
|
|
222
|
+
|
|
172
223
|
### Custom nav (advanced)
|
|
173
224
|
|
|
174
225
|
Sites that interleave several registries under a heading, or need custom
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# The SEO / social-share <head> tags: description, Open Graph, Twitter Card,
|
|
5
|
+
# canonical, favicon, robots, theme-color. Rendered by DocsUI::Shell inside
|
|
6
|
+
# <head>, driven entirely by DocsKit.configuration (+ .seo) and the per-page
|
|
7
|
+
# title/description — so every docs site is share-ready with zero markup, and a
|
|
8
|
+
# site tunes it through config alone (no Shell subclass).
|
|
9
|
+
#
|
|
10
|
+
# render DocsUI::MetaTags.new(title: "Installation", description: "…")
|
|
11
|
+
#
|
|
12
|
+
# All free text (title, description, brand) is emitted as normal Phlex
|
|
13
|
+
# attribute values, so Phlex escapes it — config free text is never trusted
|
|
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).
|
|
20
|
+
class MetaTags < Phlex::HTML
|
|
21
|
+
include Phlex::Rails::Helpers::Request
|
|
22
|
+
include Phlex::Rails::Helpers::ImageURL
|
|
23
|
+
|
|
24
|
+
# title: the page title (nil on a page that sets none, e.g. the home
|
|
25
|
+
# page) — combined with config.title_suffix for og:title.
|
|
26
|
+
# description: the page's resolved description (DocsUI::Page passes its own
|
|
27
|
+
# description or #lead); nil falls back to config.seo.description.
|
|
28
|
+
def initialize(title: nil, description: nil)
|
|
29
|
+
@title = title
|
|
30
|
+
@description = description
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def view_template
|
|
34
|
+
description_meta
|
|
35
|
+
open_graph
|
|
36
|
+
twitter_card
|
|
37
|
+
canonical_link
|
|
38
|
+
favicon_link
|
|
39
|
+
robots_meta
|
|
40
|
+
theme_color_meta
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def config = DocsKit.configuration
|
|
46
|
+
def seo = config.seo
|
|
47
|
+
|
|
48
|
+
# The page description, falling back to the site-wide default. nil → the
|
|
49
|
+
# description tags (meta description, og:description) are omitted.
|
|
50
|
+
def description = @description || seo.description
|
|
51
|
+
|
|
52
|
+
# The full title used for og:title, identical to what Shell puts in <title>:
|
|
53
|
+
# "Page · Suffix", or just the suffix on a title-less page (the home page).
|
|
54
|
+
def full_title
|
|
55
|
+
[@title, config.title_suffix].compact.join(" · ")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def description_meta
|
|
59
|
+
return unless description
|
|
60
|
+
|
|
61
|
+
meta(name: "description", content: description)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The always-present minimal OG block (title/type/site_name) plus the opt-in
|
|
65
|
+
# description/image/url. A site that configures nothing still gets a valid
|
|
66
|
+
# card — never a broken empty tag.
|
|
67
|
+
def open_graph
|
|
68
|
+
meta(property: "og:title", content: full_title)
|
|
69
|
+
meta(property: "og:type", content: seo.og_type)
|
|
70
|
+
meta(property: "og:site_name", content: config.brand)
|
|
71
|
+
open_graph_optional
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The OG tags emitted only when their value resolves — kept separate so the
|
|
75
|
+
# always-present block above stays a flat, obvious minimum.
|
|
76
|
+
def open_graph_optional
|
|
77
|
+
meta(property: "og:locale", content: seo.locale) if seo.locale
|
|
78
|
+
meta(property: "og:description", content: description) if description
|
|
79
|
+
meta(property: "og:image", content: og_image_url) if og_image_url
|
|
80
|
+
meta(property: "og:url", content: canonical_url) if canonical_url
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def twitter_card
|
|
84
|
+
meta(name: "twitter:card", content: seo.twitter_card)
|
|
85
|
+
meta(name: "twitter:site", content: seo.twitter_site) if seo.twitter_site
|
|
86
|
+
meta(name: "twitter:creator", content: seo.twitter_creator) if seo.twitter_creator
|
|
87
|
+
meta(name: "twitter:image", content: og_image_url) if og_image_url
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def canonical_link
|
|
91
|
+
return unless canonical_url
|
|
92
|
+
|
|
93
|
+
link(rel: "canonical", href: canonical_url)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def favicon_link
|
|
97
|
+
return unless seo.favicon
|
|
98
|
+
|
|
99
|
+
link(rel: "icon", href: seo.favicon)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def robots_meta
|
|
103
|
+
return unless seo.robots
|
|
104
|
+
|
|
105
|
+
meta(name: "robots", content: seo.robots)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def theme_color_meta
|
|
109
|
+
return unless seo.theme_color
|
|
110
|
+
|
|
111
|
+
meta(name: "theme-color", content: seo.theme_color)
|
|
112
|
+
end
|
|
113
|
+
|
|
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.
|
|
123
|
+
def og_image_url
|
|
124
|
+
image = seo.og_image
|
|
125
|
+
return if image.to_s.empty?
|
|
126
|
+
return image if absolute?(image)
|
|
127
|
+
|
|
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)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The canonical/og:url for this page. From config.seo.site_url when set (its
|
|
148
|
+
# path is honored verbatim so a site can point canonical at a specific URL),
|
|
149
|
+
# else the request's original URL. nil when neither is available (an isolated
|
|
150
|
+
# render), so canonical/og:url are simply omitted.
|
|
151
|
+
def canonical_url
|
|
152
|
+
return seo.site_url if seo.site_url
|
|
153
|
+
return unless request?
|
|
154
|
+
|
|
155
|
+
request.original_url
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def absolute?(url) = url.to_s.match?(%r{\Ahttps?://}i)
|
|
159
|
+
|
|
160
|
+
# True only when there's a live Rails view context AND a request on it — the
|
|
161
|
+
# phlex-rails #request helper delegates to view_context, which raises without
|
|
162
|
+
# one. Mirrors Shell#csp_nonce's guard so an isolated Phlex render (the specs,
|
|
163
|
+
# a request-less static build) degrades cleanly instead of raising.
|
|
164
|
+
def request? = !view_context.nil? && !request.nil?
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -36,6 +36,15 @@ module DocsUI
|
|
|
36
36
|
@eyebrow
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# The per-page SEO/social description → DocsUI::MetaTags (via Shell). Set it
|
|
40
|
+
# for a hand-tuned description; when unset, Page derives one from #lead, so
|
|
41
|
+
# an existing page gets a sensible description for free. nil (no
|
|
42
|
+
# description, no lead) falls back to config.seo.description in MetaTags.
|
|
43
|
+
def description(value = nil)
|
|
44
|
+
@description = value if value
|
|
45
|
+
@description
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
# The "On this page" auto-TOC placement for this page. Defaults to the
|
|
40
49
|
# configured DocsKit.configuration.on_page_default; set false to opt out,
|
|
41
50
|
# or :panel/:toggle/:sidebar to override per page.
|
|
@@ -46,7 +55,11 @@ module DocsUI
|
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
def view_template
|
|
49
|
-
render DocsUI::Shell.new(
|
|
58
|
+
render DocsUI::Shell.new(
|
|
59
|
+
title: self.class.title,
|
|
60
|
+
description: self.class.description || lead,
|
|
61
|
+
on_page: self.class.on_page
|
|
62
|
+
) do
|
|
50
63
|
# data-md-skip drops this nav from the Markdown export — it's chrome, not
|
|
51
64
|
# page content (DocsKit::MarkdownExport strips [data-md-skip]). The
|
|
52
65
|
# "Markdown" action sits opposite "← Home"; it's chrome too, so it lives
|
|
@@ -23,11 +23,16 @@ module DocsUI
|
|
|
23
23
|
|
|
24
24
|
DRAWER_ID = "site-drawer"
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
26
|
+
# title: the page title → <title> and og:title.
|
|
27
|
+
# description: the page description → the SEO/social meta tags (DocsUI::Page
|
|
28
|
+
# passes its own description or #lead); nil falls back to
|
|
29
|
+
# config.seo.description in DocsUI::MetaTags.
|
|
30
|
+
# on_page: the auto-TOC placement for this page (:panel/:toggle/:sidebar or
|
|
31
|
+
# false). Threaded to the sidebar's docs-nav controller;
|
|
32
|
+
# :panel/:toggle also render an "On this page" slot in the content.
|
|
33
|
+
def initialize(title: nil, description: nil, on_page: false)
|
|
30
34
|
@title = title
|
|
35
|
+
@description = description
|
|
31
36
|
@on_page = DocsKit.configuration.normalize_on_page(on_page)
|
|
32
37
|
end
|
|
33
38
|
|
|
@@ -72,6 +77,11 @@ module DocsUI
|
|
|
72
77
|
title { [@title, config.title_suffix].compact.join(" · ") }
|
|
73
78
|
meta(charset: "utf-8")
|
|
74
79
|
meta(name: "viewport", content: "width=device-width,initial-scale=1")
|
|
80
|
+
# SEO / social-share tags (description, Open Graph, Twitter Card,
|
|
81
|
+
# canonical, favicon, robots, theme-color) from config.seo + this page's
|
|
82
|
+
# title/description. A site that sets no c.seo still gets a valid minimal
|
|
83
|
+
# OG block, so the head is a strict superset of the pre-SEO markup.
|
|
84
|
+
render DocsUI::MetaTags.new(title: @title, description: @description)
|
|
75
85
|
csrf_meta_tags
|
|
76
86
|
csp_meta_tag
|
|
77
87
|
# Turbo morphs page-level navigations so a re-render preserves scroll and
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "seo_config"
|
|
4
|
+
|
|
3
5
|
module DocsKit
|
|
4
6
|
# Per-site configuration for the shared docs chrome. Everything that differs
|
|
5
7
|
# between two otherwise-identical docs sites lives here, so the Phlex shell
|
|
@@ -270,6 +272,15 @@ module DocsKit
|
|
|
270
272
|
Array(@topbar_links).map { |link| DocsKit::TopbarLink.from(link) }
|
|
271
273
|
end
|
|
272
274
|
|
|
275
|
+
# The SEO / social-share knobs (DocsKit::SeoConfig), read by DocsUI::MetaTags.
|
|
276
|
+
# Lazily built and memoized so a `c.seo.description = ...` block mutates the
|
|
277
|
+
# one instance the Shell later reads. A site that never touches it gets the
|
|
278
|
+
# backwards-safe defaults (see SeoConfig), so the head is a strict superset of
|
|
279
|
+
# the pre-SEO markup.
|
|
280
|
+
def seo
|
|
281
|
+
@seo ||= DocsKit::SeoConfig.new
|
|
282
|
+
end
|
|
283
|
+
|
|
273
284
|
# The loaded DocsKit::OpenApi::Document for #openapi. Memoized; when #openapi
|
|
274
285
|
# is a file path, the memo is invalidated on an mtime change so editing the
|
|
275
286
|
# spec in development is picked up without a server restart. Raises a
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module DocsKit
|
|
6
|
+
# Screenshots a URL into the site's OG/Twitter images. Loaded ONLY by the
|
|
7
|
+
# host-installed `docs_kit:og` rake task (via an explicit require) — never at
|
|
8
|
+
# gem runtime — so the headless-browser tooling it drives is never a docs-kit
|
|
9
|
+
# dependency. zeitwerk ignores this file (see lib/docs_kit.rb) for the same
|
|
10
|
+
# reason: it must not be eager-loaded into a host that never runs the task.
|
|
11
|
+
#
|
|
12
|
+
# Usage (from the rake task):
|
|
13
|
+
# DocsKit::OgGenerator.new(url:, out_dir:, sizes:, shooter:).call
|
|
14
|
+
#
|
|
15
|
+
# When #url is nil the generator boots the Rails app on an ephemeral port and
|
|
16
|
+
# shoots "/"; when set it shoots that URL directly (e.g. a deployed site). The
|
|
17
|
+
# shooter is a headless-browser CLI resolved at runtime: an explicit override,
|
|
18
|
+
# else the first of shot-scraper / chromium / chrome found on PATH.
|
|
19
|
+
class OgGenerator
|
|
20
|
+
# Raised when no supported headless-browser CLI is available. The message
|
|
21
|
+
# names each supported tool + how to install one, so the task fails with a
|
|
22
|
+
# fix, not a stack trace.
|
|
23
|
+
class NoShooterError < StandardError; end
|
|
24
|
+
|
|
25
|
+
# The shooters we know how to drive, in preference order. shot-scraper first
|
|
26
|
+
# (purpose-built, handles sizing cleanly); then a raw chromium/chrome.
|
|
27
|
+
SHOOTERS = %w[shot-scraper chromium chromium-browser google-chrome chrome].freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :url, :out_dir, :sizes
|
|
30
|
+
|
|
31
|
+
def initialize(url:, out_dir:, sizes:, shooter: nil)
|
|
32
|
+
@url = url
|
|
33
|
+
@out_dir = out_dir.to_s
|
|
34
|
+
@sizes = sizes
|
|
35
|
+
@shooter = shooter
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The chosen shooter name (explicit override or nil until #resolve_shooter!).
|
|
39
|
+
def shooter_name = @shooter
|
|
40
|
+
|
|
41
|
+
# The full run: resolve a shooter, ensure the output dir, boot a local server
|
|
42
|
+
# if needed, and shoot every size. Returns the list of written paths.
|
|
43
|
+
def call
|
|
44
|
+
tool = resolve_shooter!
|
|
45
|
+
require "fileutils"
|
|
46
|
+
FileUtils.mkdir_p(out_dir)
|
|
47
|
+
|
|
48
|
+
with_target_url do |target|
|
|
49
|
+
sizes.map do |filename, dimensions|
|
|
50
|
+
path = File.join(out_dir, filename)
|
|
51
|
+
run(command_for(tool, target, dimensions, path))
|
|
52
|
+
path
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Resolve the shooter CLI: an explicit override (returned as-is so a user can
|
|
58
|
+
# force a specific tool), else the first supported binary on PATH. Raises
|
|
59
|
+
# NoShooterError naming the options when none is found.
|
|
60
|
+
def resolve_shooter!
|
|
61
|
+
return @shooter if @shooter && !@shooter.to_s.empty?
|
|
62
|
+
|
|
63
|
+
found = SHOOTERS.find { |cmd| which(cmd) }
|
|
64
|
+
return found if found
|
|
65
|
+
|
|
66
|
+
raise NoShooterError,
|
|
67
|
+
"No headless-browser CLI found. Install one of: shot-scraper " \
|
|
68
|
+
"(`pipx install shot-scraper && shot-scraper install`), chromium, or " \
|
|
69
|
+
"chrome — or set DOCS_KIT_SHOT to the command to use."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build the argv for `tool` to screenshot `target` at `[w, h]` into `path`.
|
|
73
|
+
# Kept pure (no side effects) so it's unit-testable without a browser.
|
|
74
|
+
def command_for(tool, target, dimensions, path)
|
|
75
|
+
width, height = dimensions
|
|
76
|
+
case tool
|
|
77
|
+
when "shot-scraper"
|
|
78
|
+
["shot-scraper", target, "--width", width.to_s, "--height", height.to_s, "-o", path]
|
|
79
|
+
else # a chromium/chrome family binary
|
|
80
|
+
[
|
|
81
|
+
tool, "--headless=new", "--disable-gpu", "--hide-scrollbars",
|
|
82
|
+
"--force-device-scale-factor=1",
|
|
83
|
+
"--window-size=#{width},#{height}",
|
|
84
|
+
"--screenshot=#{path}", target
|
|
85
|
+
]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Yield the URL to shoot. If #url was given, shoot it directly. Otherwise boot
|
|
92
|
+
# the host Rails app on an ephemeral port, yield its local URL, and tear it
|
|
93
|
+
# down after. Rack/rackup are the host app's own deps (it's a Rails app), not
|
|
94
|
+
# docs-kit's — required lazily here so the gem never loads them.
|
|
95
|
+
def with_target_url(&)
|
|
96
|
+
return yield(url) if url && !url.to_s.empty?
|
|
97
|
+
|
|
98
|
+
boot_local_server(&)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Boot the Rails app with Rack::Handler on an ephemeral port in a background
|
|
102
|
+
# thread, wait for it to answer, yield the URL, then shut down.
|
|
103
|
+
def boot_local_server
|
|
104
|
+
require "rack"
|
|
105
|
+
require "socket"
|
|
106
|
+
|
|
107
|
+
port = free_port
|
|
108
|
+
app = Rails.application
|
|
109
|
+
server_thread = Thread.new do
|
|
110
|
+
Rackup::Handler.get("webrick").run(app, Port: port, Host: "127.0.0.1", Logger: null_logger, AccessLog: [])
|
|
111
|
+
rescue LoadError
|
|
112
|
+
Rack::Handler.get("webrick").run(app, Port: port, Host: "127.0.0.1", Logger: null_logger, AccessLog: [])
|
|
113
|
+
end
|
|
114
|
+
wait_for_port(port)
|
|
115
|
+
yield "http://127.0.0.1:#{port}/"
|
|
116
|
+
ensure
|
|
117
|
+
server_thread&.kill
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# An open ephemeral port (bind to 0, read it back, close). A tiny race window
|
|
121
|
+
# remains but is acceptable for a one-shot local screenshot server.
|
|
122
|
+
def free_port
|
|
123
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
124
|
+
port = server.addr[1]
|
|
125
|
+
server.close
|
|
126
|
+
port
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Poll until the local server accepts a connection (or give up after ~15s so a
|
|
130
|
+
# boot failure surfaces instead of hanging).
|
|
131
|
+
def wait_for_port(port, timeout: 15)
|
|
132
|
+
deadline = monotonic_now + timeout
|
|
133
|
+
loop do
|
|
134
|
+
TCPSocket.new("127.0.0.1", port).close
|
|
135
|
+
return
|
|
136
|
+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
|
|
137
|
+
raise "docs_kit:og: local server did not start within #{timeout}s" if monotonic_now > deadline
|
|
138
|
+
|
|
139
|
+
sleep 0.2
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
144
|
+
|
|
145
|
+
def null_logger
|
|
146
|
+
require "logger"
|
|
147
|
+
Logger.new(File::NULL)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Run a command, raising with the joined argv on a non-zero exit so a shooter
|
|
151
|
+
# failure is loud (not a silently-missing image).
|
|
152
|
+
def run(argv)
|
|
153
|
+
ok = system(*argv)
|
|
154
|
+
raise "docs_kit:og: screenshot command failed: #{argv.shelljoin}" unless ok
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Locate an executable on PATH (a dependency-free `which`), returning its full
|
|
158
|
+
# path or nil. Overridable in specs so shooter resolution is testable.
|
|
159
|
+
def which(cmd)
|
|
160
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
161
|
+
candidate = File.join(dir, cmd)
|
|
162
|
+
return candidate if File.executable?(candidate) && !File.directory?(candidate)
|
|
163
|
+
end
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
# The per-site SEO / social-share knobs, read by DocsUI::MetaTags to emit the
|
|
5
|
+
# <head> meta/link tags (description, Open Graph, Twitter Card, canonical,
|
|
6
|
+
# favicon, theme-color). Nested under DocsKit::Configuration#seo so a site
|
|
7
|
+
# configures it as a small block:
|
|
8
|
+
#
|
|
9
|
+
# DocsKit.configure do |c|
|
|
10
|
+
# c.seo.description = "What these docs cover, in one sentence."
|
|
11
|
+
# c.seo.og_image = "og/og.png" # generated by `bin/rails docs_kit:og`
|
|
12
|
+
# c.seo.twitter_site = "@my_handle"
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Every field defaults to a backwards-safe value: a site that sets none still
|
|
16
|
+
# renders a valid minimal Open Graph block and no broken empty tags. Plain
|
|
17
|
+
# accessors (not Data.define) because each field is individually assignable in
|
|
18
|
+
# the `c.seo.x = ...` block, mirroring DocsKit::Configuration's own style.
|
|
19
|
+
class SeoConfig
|
|
20
|
+
# A one-line page/site description → <meta name="description"> and
|
|
21
|
+
# og:description / twitter:description. nil omits the description tags
|
|
22
|
+
# (DocsUI::Page derives a per-page value from #lead when a page sets none).
|
|
23
|
+
attr_accessor :description
|
|
24
|
+
|
|
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.
|
|
32
|
+
attr_accessor :og_image
|
|
33
|
+
|
|
34
|
+
# The og:type. "website" for a docs site; a page could override to "article".
|
|
35
|
+
attr_accessor :og_type
|
|
36
|
+
|
|
37
|
+
# The twitter:card style. "summary_large_image" renders a full-width banner
|
|
38
|
+
# (size your og:image 1200×630 for it); "summary" is the small square card.
|
|
39
|
+
attr_accessor :twitter_card
|
|
40
|
+
|
|
41
|
+
# The site's @handle → twitter:site, and the content author's → twitter:creator.
|
|
42
|
+
# Both nil by default (a site with no X presence emits neither tag).
|
|
43
|
+
attr_accessor :twitter_site, :twitter_creator
|
|
44
|
+
|
|
45
|
+
# The og:locale (e.g. "en_US", "sv_SE").
|
|
46
|
+
attr_accessor :locale
|
|
47
|
+
|
|
48
|
+
# The site's canonical base URL (e.g. "https://docs.example.com"). When set,
|
|
49
|
+
# og:image/og:url/canonical are absolutized against it even off a request
|
|
50
|
+
# (a static build, an isolated render). nil → DocsUI::MetaTags falls back to
|
|
51
|
+
# the request base URL, and omits canonical/absolute og:url when there's no
|
|
52
|
+
# request either.
|
|
53
|
+
attr_accessor :site_url
|
|
54
|
+
|
|
55
|
+
# A robots directive (e.g. "noindex, nofollow") → <meta name="robots">. nil
|
|
56
|
+
# omits the tag, so the page is indexable by default.
|
|
57
|
+
attr_accessor :favicon
|
|
58
|
+
|
|
59
|
+
# A favicon asset path/URL → <link rel="icon">. nil omits the link (the host
|
|
60
|
+
# may serve /favicon.ico itself).
|
|
61
|
+
attr_accessor :robots
|
|
62
|
+
|
|
63
|
+
# A theme-color (e.g. "#0f172a") → <meta name="theme-color">, tinting mobile
|
|
64
|
+
# browser chrome. nil omits the tag.
|
|
65
|
+
attr_accessor :theme_color
|
|
66
|
+
|
|
67
|
+
def initialize
|
|
68
|
+
@description = nil
|
|
69
|
+
@og_image = nil
|
|
70
|
+
@og_type = "website"
|
|
71
|
+
@twitter_card = "summary_large_image"
|
|
72
|
+
@twitter_site = nil
|
|
73
|
+
@twitter_creator = nil
|
|
74
|
+
@locale = "en_US"
|
|
75
|
+
@site_url = nil
|
|
76
|
+
@robots = nil
|
|
77
|
+
@favicon = nil
|
|
78
|
+
@theme_color = nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -169,6 +169,9 @@ after_bundle do
|
|
|
169
169
|
Next:
|
|
170
170
|
cd #{app_name}
|
|
171
171
|
bin/dev # or bin/rails server
|
|
172
|
+
bin/rails docs_kit:og # generate your social-share image from the landing page
|
|
173
|
+
# (needs a headless browser: shot-scraper or chromium)
|
|
174
|
+
SEO: set c.seo.* in config/initializers/docs_kit.rb (description, og_image, twitter_site).
|
|
172
175
|
Deploy: push, create a GitHub Release (or run the Deploy docs workflow).
|
|
173
176
|
Set repo secrets: SSH_PRIVATE_KEY, DEPLOY_HOST, DEPLOY_DOMAIN (env: docs).
|
|
174
177
|
MSG
|
data/lib/docs_kit/version.rb
CHANGED
data/lib/docs_kit.rb
CHANGED
|
@@ -54,6 +54,13 @@ loader.push_dir(File.expand_path("docs_kit", __dir__), namespace: DocsKit)
|
|
|
54
54
|
loader.push_dir(File.expand_path("../app/components/docs_ui", __dir__), namespace: DocsUI)
|
|
55
55
|
loader.ignore(File.expand_path("docs_kit/version.rb", __dir__))
|
|
56
56
|
loader.ignore(File.expand_path("docs_kit/configuration.rb", __dir__))
|
|
57
|
+
# Required eagerly by configuration.rb (a plain-Ruby value object, no Rails), so
|
|
58
|
+
# ignore it here too or zeitwerk double-manages the constant.
|
|
59
|
+
loader.ignore(File.expand_path("docs_kit/seo_config.rb", __dir__))
|
|
60
|
+
# Loaded ONLY by the host's docs_kit:og rake task (an explicit require), never at
|
|
61
|
+
# gem runtime — so its Rack/browser tooling is never pulled into a host that
|
|
62
|
+
# doesn't run the task. Ignore it so eager_load! doesn't require it.
|
|
63
|
+
loader.ignore(File.expand_path("docs_kit/og_generator.rb", __dir__))
|
|
57
64
|
# docs_kit/rubocop.rb is the RuboCop-cop entry point: it defines cops under
|
|
58
65
|
# RuboCop::Cop::DocsKit::*, not a DocsKit::Rubocop constant, so zeitwerk must not
|
|
59
66
|
# manage it. It (and the cops under lib/rubocop/, which are outside the loader's
|
|
@@ -168,6 +168,16 @@ module DocsKit
|
|
|
168
168
|
create_file "app/assets/builds/.keep", ""
|
|
169
169
|
end
|
|
170
170
|
|
|
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
|
+
def create_og_task
|
|
178
|
+
template "docs_kit_og.rake", "lib/tasks/docs_kit_og.rake"
|
|
179
|
+
end
|
|
180
|
+
|
|
171
181
|
def wire_assets_and_package_json
|
|
172
182
|
# Serve the bun-built CSS from app/assets/builds.
|
|
173
183
|
inject_into_file "config/initializers/assets.rb",
|
|
@@ -30,6 +30,28 @@ Rails.application.config.to_prepare do
|
|
|
30
30
|
# # { href: "https://discord.gg/INVITE", label: "Discord", icon: :discord },
|
|
31
31
|
# ]
|
|
32
32
|
|
|
33
|
+
# SEO + social sharing. docs-kit emits a full <head> (description, Open Graph,
|
|
34
|
+
# Twitter Card, canonical, favicon, theme-color) from these knobs — a shared
|
|
35
|
+
# link renders a rich card with zero markup. Every field is optional; a site
|
|
36
|
+
# that sets none still gets a valid minimal card. Per-page descriptions come
|
|
37
|
+
# from each page's `description "..."` (falling back to its lead paragraph).
|
|
38
|
+
# c.seo.description = "What these docs cover, in one sentence."
|
|
39
|
+
# c.seo.twitter_card = "summary_large_image" # default; "summary" = small card
|
|
40
|
+
# c.seo.twitter_site = "@your_handle"
|
|
41
|
+
# c.seo.twitter_creator = "@author_handle"
|
|
42
|
+
# c.seo.site_url = "https://docs.example.com" # your canonical base URL
|
|
43
|
+
# c.seo.locale = "en_US"
|
|
44
|
+
# c.seo.robots = "noindex, nofollow" # keep a staging/private site out of search
|
|
45
|
+
# c.seo.favicon = "/favicon.ico"
|
|
46
|
+
# c.seo.theme_color = "#0f172a" # tints mobile browser chrome
|
|
47
|
+
#
|
|
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).
|
|
52
|
+
# bin/rails docs_kit:og
|
|
53
|
+
# c.seo.og_image = "og/og.png" # a path in app/assets/images/
|
|
54
|
+
|
|
33
55
|
# Code blocks use one Rouge theme by default. To keep them readable in BOTH
|
|
34
56
|
# light and dark daisyUI themes, set a light base + a dark override — the dark
|
|
35
57
|
# theme's CSS is scoped under [data-theme=X] for each shipped dark theme, so
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generate this site's social-share (Open Graph / Twitter) images by
|
|
4
|
+
# screenshotting its OWN landing page, so a shared link renders a real card of
|
|
5
|
+
# your docs — not the neutral placeholder docs-kit ships. Run it whenever the
|
|
6
|
+
# landing page changes materially:
|
|
7
|
+
#
|
|
8
|
+
# bin/rails docs_kit:og
|
|
9
|
+
#
|
|
10
|
+
# It boots the app, serves it locally, screenshots "/" at the standard sizes, and
|
|
11
|
+
# writes them into app/assets/images/og/. Point c.seo.og_image at the result
|
|
12
|
+
# (the default "og/og.png" already matches). This is a documented, manual routine
|
|
13
|
+
# (like phlex-reactive's vendored-client re-sync) — never run automatically, so a
|
|
14
|
+
# machine without a headless browser is never blocked at deploy time.
|
|
15
|
+
#
|
|
16
|
+
# It shells out to a headless-browser CLI you already have; it is NOT a docs-kit
|
|
17
|
+
# runtime dependency. Supported (auto-detected, first one found wins), override
|
|
18
|
+
# with DOCS_KIT_SHOT:
|
|
19
|
+
# * shot-scraper (https://shot-scraper.datasette.io) — `pipx install shot-scraper`
|
|
20
|
+
# * chromium/chrome headless --screenshot
|
|
21
|
+
# Set DOCS_KIT_OG_URL to shoot a deployed URL instead of booting locally.
|
|
22
|
+
namespace :docs_kit do
|
|
23
|
+
desc "Screenshot the landing page into app/assets/images/og/{og,twitter,square}.png"
|
|
24
|
+
task og: :environment do
|
|
25
|
+
require "docs_kit/og_generator"
|
|
26
|
+
|
|
27
|
+
sizes = {
|
|
28
|
+
"og.png" => [1200, 630], # Open Graph / twitter summary_large_image
|
|
29
|
+
"twitter.png" => [1024, 512], # Twitter summary card
|
|
30
|
+
"square.png" => [600, 600] # square fallback (some chat clients)
|
|
31
|
+
}
|
|
32
|
+
out_dir = Rails.root.join("app/assets/images/og")
|
|
33
|
+
|
|
34
|
+
DocsKit::OgGenerator.new(
|
|
35
|
+
url: ENV.fetch("DOCS_KIT_OG_URL", nil),
|
|
36
|
+
out_dir: out_dir,
|
|
37
|
+
sizes: sizes,
|
|
38
|
+
shooter: ENV.fetch("DOCS_KIT_SHOT", nil)
|
|
39
|
+
).call
|
|
40
|
+
|
|
41
|
+
puts "✅ Wrote #{sizes.keys.join(', ')} to #{out_dir.to_s.sub("#{Dir.pwd}/", '')}"
|
|
42
|
+
puts " Point c.seo.og_image at one of them (default \"og/og.png\" already does)."
|
|
43
|
+
end
|
|
44
|
+
end
|
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
|
|
@@ -176,6 +176,7 @@ files:
|
|
|
176
176
|
- app/components/docs_ui/json_response.rb
|
|
177
177
|
- app/components/docs_ui/markdown.rb
|
|
178
178
|
- app/components/docs_ui/markdown_action.rb
|
|
179
|
+
- app/components/docs_ui/meta_tags.rb
|
|
179
180
|
- app/components/docs_ui/on_this_page.rb
|
|
180
181
|
- app/components/docs_ui/open_api_operation.rb
|
|
181
182
|
- app/components/docs_ui/page.rb
|
|
@@ -214,6 +215,7 @@ files:
|
|
|
214
215
|
- lib/docs_kit/mcp_server.rb
|
|
215
216
|
- lib/docs_kit/mcp_tools.rb
|
|
216
217
|
- lib/docs_kit/nav_item.rb
|
|
218
|
+
- lib/docs_kit/og_generator.rb
|
|
217
219
|
- lib/docs_kit/open_api.rb
|
|
218
220
|
- lib/docs_kit/open_api/document.rb
|
|
219
221
|
- lib/docs_kit/open_api/operation.rb
|
|
@@ -223,6 +225,7 @@ files:
|
|
|
223
225
|
- lib/docs_kit/search_hit.rb
|
|
224
226
|
- lib/docs_kit/search_index.rb
|
|
225
227
|
- lib/docs_kit/search_index/snippet.rb
|
|
228
|
+
- lib/docs_kit/seo_config.rb
|
|
226
229
|
- lib/docs_kit/shortcut.rb
|
|
227
230
|
- lib/docs_kit/templates/new_site.rb
|
|
228
231
|
- lib/docs_kit/topbar_link.rb
|
|
@@ -237,6 +240,7 @@ files:
|
|
|
237
240
|
- lib/generators/docs_kit/install/templates/doc.rb.erb
|
|
238
241
|
- lib/generators/docs_kit/install/templates/docs_controller.rb.erb
|
|
239
242
|
- lib/generators/docs_kit/install/templates/docs_kit.rb.erb
|
|
243
|
+
- lib/generators/docs_kit/install/templates/docs_kit_og.rake
|
|
240
244
|
- lib/generators/docs_kit/install/templates/installation_page.rb.erb
|
|
241
245
|
- lib/generators/docs_kit/install/templates/landing.rb.erb
|
|
242
246
|
- lib/generators/docs_kit/install/templates/landings_controller.rb.erb
|