docs-kit 1.0.8 → 1.1.0
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/README.md +47 -20
- data/app/components/docs_ui/archived_page.rb +45 -0
- data/app/components/docs_ui/brand_mark.rb +1 -2
- data/app/controllers/docs_kit/llms_controller.rb +8 -1
- data/app/controllers/docs_kit/mcp_controller.rb +5 -1
- data/app/controllers/docs_kit/search_controller.rb +6 -1
- data/exe/docs-kit +5 -5
- data/lib/docs_kit/configuration.rb +89 -4
- data/lib/docs_kit/controller.rb +11 -2
- data/lib/docs_kit/doc_version.rb +59 -0
- data/lib/docs_kit/llms_text.rb +31 -4
- data/lib/docs_kit/markdown_export/blocks.rb +3 -2
- data/lib/docs_kit/mcp_tools.rb +2 -1
- data/lib/docs_kit/registry.rb +7 -0
- data/lib/docs_kit/scope.rb +59 -0
- data/lib/docs_kit/scoping.rb +28 -0
- data/lib/docs_kit/snapshot/entry.rb +48 -0
- data/lib/docs_kit/snapshot.rb +151 -0
- data/lib/docs_kit/templates/new_site.rb +64 -12
- data/lib/docs_kit/version.rb +1 -1
- data/lib/generators/docs_kit/install/install_generator.rb +1 -1
- data/lib/generators/docs_kit/install/templates/Dockerfile.tt +5 -5
- data/lib/generators/docs_kit/install/templates/agents_md.erb +1 -1
- data/lib/generators/docs_kit/install/templates/dockerignore +1 -0
- data/lib/generators/docs_kit/install/templates/skill.md.erb +1 -1
- metadata +11 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b53c8fcfcdc8d5053f9019ea56edc5a1b4fce1a8760a220eb19c76a297e72d53
|
|
4
|
+
data.tar.gz: 4289ad366b8462a9617db0e696250b717c21af1a30a97126367c370452156e1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80f49ed3f55338303fe09ebed720679c440eaadb02a99b112b312dfebafa70b081ccbf3622f20a7a0a6b6f48771969528746465377bda9961b62ce6cf9410521
|
|
7
|
+
data.tar.gz: 3dc20bc085a7baa55b0c64098931f03d00553b26e477a606ce1fe878850d96058992cd7ef3e4cbc59bc67c1ea07eb9be26219f2d1c5f7b83a04eb22e82737c47
|
data/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# docs-kit
|
|
2
2
|
|
|
3
|
-
[](https://github.com/zoolutions/docs-kit/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
5
|
Shared [Phlex](https://www.phlex.fun) chrome for documentation sites built on
|
|
6
6
|
[daisyUI](https://daisyui.com). Extract the shell, sidebar, code blocks, theme
|
|
7
7
|
switcher, and page kit into one gem so multiple docs sites look identical and are
|
|
8
8
|
maintained in one place.
|
|
9
9
|
|
|
10
|
-
Reactive demos ([phlex-reactive](https://github.com/
|
|
11
|
-
and Postgres-SSE transport ([pgbus](https://github.com/
|
|
10
|
+
Reactive demos ([phlex-reactive](https://github.com/zoolutions/phlex-reactive))
|
|
11
|
+
and Postgres-SSE transport ([pgbus](https://github.com/zoolutions/pgbus)) are
|
|
12
12
|
**optional, runtime-detected** add-ons — docs-kit does not depend on them.
|
|
13
13
|
|
|
14
14
|
## What you get
|
|
@@ -137,7 +137,7 @@ fronts Puma with Thruster (`CMD ["./bin/thrust", "./bin/rails", "server"]`) for
|
|
|
137
137
|
HTTP caching, compression, and X-Sendfile — and the generator scaffolds the
|
|
138
138
|
`bin/thrust` binstub if the app lacks one, since the exec-form CMD needs the
|
|
139
139
|
file to exist in the image. Thruster listens on the routed port
|
|
140
|
-
(`HTTP_PORT=3000` —
|
|
140
|
+
(`HTTP_PORT=3000` — dash's `app_port`) and proxies to Puma on `TARGET_PORT=3001`.
|
|
141
141
|
Without thruster in the *production* bundle (absent, or only in a
|
|
142
142
|
development/test group that `BUNDLE_WITHOUT` excludes) the CMD falls back to
|
|
143
143
|
plain `rails server` — never a thrust CMD that would crash at boot.
|
|
@@ -701,7 +701,7 @@ claude mcp add --transport http docs https://your-docs.example/mcp
|
|
|
701
701
|
|
|
702
702
|
and can ask Claude to search or read your docs, which now appear as tools. The
|
|
703
703
|
JSON-RPC is stateless (each `POST` is independent — no SSE session), so it works
|
|
704
|
-
behind the existing
|
|
704
|
+
behind the existing dash/Cloudflare deploy unchanged; `GET`/`DELETE` return
|
|
705
705
|
`405`. When enabled, `/llms.txt` grows a final `## MCP` line advertising the
|
|
706
706
|
endpoint so agents discover it.
|
|
707
707
|
|
|
@@ -844,7 +844,7 @@ back to a spec.
|
|
|
844
844
|
|
|
845
845
|
```bash
|
|
846
846
|
docs-kit new my-docs # → a complete, deployable docs app
|
|
847
|
-
docs-kit new my-docs --image
|
|
847
|
+
docs-kit new my-docs --image zoolutions/my-repo --service my-repo
|
|
848
848
|
```
|
|
849
849
|
|
|
850
850
|
`docs-kit new` runs `rails new` (propshaft + importmap + turbo/stimulus, no DB)
|
|
@@ -854,7 +854,7 @@ and applies docs-kit's application template, which:
|
|
|
854
854
|
- runs `rails g docs_kit:install` (initializers, controllers, a Doc registry, a
|
|
855
855
|
sample guide page, the Bun/Tailwind build, the docs-nav Stimulus wiring),
|
|
856
856
|
- syncs the lucide icons and builds the CSS,
|
|
857
|
-
- scaffolds
|
|
857
|
+
- scaffolds dash (`config/deploy.yml`, `.dash/secrets`, an optimized
|
|
858
858
|
multi-stage `Dockerfile` + a `.dockerignore`) and a thin
|
|
859
859
|
`.github/workflows/deploy-docs.yml` that calls the reusable workflow.
|
|
860
860
|
|
|
@@ -905,7 +905,7 @@ group. Then `bundle exec rubocop` runs the docs-kit cops.
|
|
|
905
905
|
The build + deploy is defined **once** in this gem's reusable workflow
|
|
906
906
|
(`.github/workflows/deploy.yml`). `docs-kit new` scaffolds the caller for you; to
|
|
907
907
|
wire it by hand a site adds five small things and it deploys to the
|
|
908
|
-
oss-infrastructure server (
|
|
908
|
+
oss-infrastructure server (dash + GHCR + Cloudflare Tunnel).
|
|
909
909
|
|
|
910
910
|
**1. A thin caller** — `.github/workflows/deploy-docs.yml`:
|
|
911
911
|
|
|
@@ -916,46 +916,73 @@ on:
|
|
|
916
916
|
workflow_dispatch:
|
|
917
917
|
jobs:
|
|
918
918
|
deploy:
|
|
919
|
-
uses:
|
|
919
|
+
uses: zoolutions/docs-kit/.github/workflows/deploy.yml@main
|
|
920
920
|
with:
|
|
921
|
-
image:
|
|
921
|
+
image: zoolutions/<repo> # OWNER/REPO — see naming note below
|
|
922
922
|
service: <repo>
|
|
923
923
|
secrets: inherit
|
|
924
924
|
```
|
|
925
925
|
|
|
926
|
-
**2. `docs/config/deploy.yml`** — `service:` and `image:` MUST match the caller
|
|
926
|
+
**2. `docs/config/deploy.yml`** — `service:` and `image:` MUST match the caller
|
|
927
|
+
(the full file `docs-kit new` writes is in `lib/docs_kit/templates/new_site.rb`;
|
|
928
|
+
`dash docs proxy` documents every key):
|
|
927
929
|
|
|
928
930
|
```yaml
|
|
929
931
|
service: <repo>
|
|
930
|
-
image:
|
|
931
|
-
|
|
932
|
+
image: zoolutions/<repo>
|
|
933
|
+
minimum_version: 4.0.0 # dash 4: dash-proxy identity + in-place host migration
|
|
934
|
+
retain_containers: 2
|
|
935
|
+
error_pages_path: public # 502/503/504.html served during a deploy gap
|
|
936
|
+
registry: { server: ghcr.io, username: mhenrixon, password: [DASH_REGISTRY_PASSWORD] }
|
|
932
937
|
builder: { arch: amd64, context: .., dockerfile: Dockerfile } # repo root = build context
|
|
933
|
-
proxy: { host: <%= ENV["DEPLOY_DOMAIN"] %>, app_port: 3000, ssl: false, healthcheck: { path: /up } }
|
|
934
938
|
servers: { web: { hosts: [<%= ENV["DEPLOY_HOST"] %>] } }
|
|
935
939
|
ssh: { user: oss }
|
|
940
|
+
proxy:
|
|
941
|
+
host: <%= ENV["DEPLOY_DOMAIN"] %>
|
|
942
|
+
app_port: 3000
|
|
943
|
+
ssl: false # TLS terminates at Cloudflare
|
|
944
|
+
healthcheck: { path: /up, interval: 5, timeout: 30 }
|
|
945
|
+
compress: true # zstd/br/gzip at the edge
|
|
946
|
+
cache: { enabled: true, max_ttl: 300 } # stores `Cache-Control: public` responses (assets, /llms*.txt)
|
|
947
|
+
headers: { response: { set: { X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin }, remove: [Server, X-Powered-By] } }
|
|
948
|
+
intercept_errors: [502, 503, 504]
|
|
949
|
+
exclude_metrics_paths: [/up]
|
|
936
950
|
```
|
|
937
951
|
|
|
952
|
+
Deliberately **not** set: `proxy.run` (`port_holder`, `log_format`, …) is
|
|
953
|
+
host-wide — every docs site on the shared host boots the same proxy, and a
|
|
954
|
+
`run:` block that differs between them makes each alternate deploy reboot it.
|
|
955
|
+
`rate_limit`/`deny_ips` need `client_ip.trusted_proxies` pinned to the tunnel's
|
|
956
|
+
address to key on visitors rather than on cloudflared; add them per site once
|
|
957
|
+
that address is known.
|
|
958
|
+
|
|
938
959
|
**3. `docs/Dockerfile`** — end the final stage with the matching label:
|
|
939
960
|
|
|
940
961
|
```dockerfile
|
|
941
962
|
LABEL service="<repo>"
|
|
942
963
|
```
|
|
943
964
|
|
|
944
|
-
**4. `docs/.
|
|
965
|
+
**4. `docs/.dash/secrets`** — `DASH_REGISTRY_PASSWORD=$DASH_REGISTRY_PASSWORD`.
|
|
945
966
|
|
|
946
967
|
**5. GitHub** — a `docs` environment with secrets `SSH_PRIVATE_KEY`,
|
|
947
968
|
`DEPLOY_HOST`, `DEPLOY_DOMAIN`. (The registry password is the auto-provided
|
|
948
969
|
`GITHUB_TOKEN` — no PAT.)
|
|
949
970
|
|
|
950
971
|
> **Naming — use the repo name, not `<repo>-docs`.** `image`/`service` must be
|
|
951
|
-
> the calling repo's `OWNER/REPO`. Pushing `ghcr.io/
|
|
972
|
+
> the calling repo's `OWNER/REPO`. Pushing `ghcr.io/zoolutions/<repo>` from the
|
|
952
973
|
> repo's own Actions run auto-links the package to the repo, so `GITHUB_TOKEN`
|
|
953
974
|
> can both push (build job) and pull (deploy) it. A different name becomes an
|
|
954
975
|
> unlinked user-scoped package `GITHUB_TOKEN` can't pull → the deploy fails.
|
|
955
976
|
|
|
956
|
-
**First deploy per host:** run `
|
|
957
|
-
any accessories (e.g. a Postgres accessory); the release workflow runs
|
|
958
|
-
`
|
|
977
|
+
**First deploy per host:** run `dash setup` (or `bin/deploy setup`) once to boot
|
|
978
|
+
any accessories (e.g. a Postgres accessory); the release workflow runs
|
|
979
|
+
`dash doctor` (a pre-flight of host, registry, proxy, ports and readiness gates)
|
|
980
|
+
and then plain `dash deploy`, which doesn't boot accessories.
|
|
981
|
+
|
|
982
|
+
**Upgrading a host to dash 4:** the first 4.x deploy renames the proxy
|
|
983
|
+
(`kamal-proxy` → `dash-proxy`, network `kamal` → `dash`, config volume copied)
|
|
984
|
+
and costs one short outage on that host while ports 80/443 change hands. It is
|
|
985
|
+
idempotent and shared by every site on the host — whichever deploys first pays it.
|
|
959
986
|
|
|
960
987
|
## CSS — the canonical build
|
|
961
988
|
|
|
@@ -1079,7 +1106,7 @@ them:
|
|
|
1079
1106
|
→ your profile → *Trusted Publishers* → *Create*, add a **pending** publisher
|
|
1080
1107
|
(works for a gem not yet pushed) with:
|
|
1081
1108
|
- Gem name: `docs-kit`
|
|
1082
|
-
- Repository: `
|
|
1109
|
+
- Repository: `zoolutions/docs-kit`
|
|
1083
1110
|
- Workflow filename: `release.yml`
|
|
1084
1111
|
- Environment: `rubygems`
|
|
1085
1112
|
2. **GitHub `rubygems` environment.** Repo *Settings → Environments → New
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# Renders one page of an ARCHIVED documentation version — a frozen Markdown
|
|
5
|
+
# body (DocsKit::Snapshot::Entry) through today's live chrome, so archived
|
|
6
|
+
# docs get every future Shell/Sidebar/Code fix for free. The live counterpart
|
|
7
|
+
# is DocsUI::Page; this mirrors its shape with the content coming from the
|
|
8
|
+
# snapshot file instead of an authored #content method.
|
|
9
|
+
#
|
|
10
|
+
# Every kwarg defaults, so even a naive `entry.view_class.new` (a custom
|
|
11
|
+
# registry predating #renderable) renders an empty page rather than raising.
|
|
12
|
+
#
|
|
13
|
+
# NOTE (issue #61 phase 4): the "you are viewing the 1.0 docs" banner with a
|
|
14
|
+
# link to the current equivalent lands with the version switcher, not here.
|
|
15
|
+
#
|
|
16
|
+
# Deliberately does NOT include Phlex::Rails::Helpers::Routes/Request — their
|
|
17
|
+
# bodies run Rails.* at class load, which would make this class (and
|
|
18
|
+
# everything referencing it, like Snapshot::Entry#view_class) unloadable in a
|
|
19
|
+
# Rails-free render. Nothing here needs a request.
|
|
20
|
+
class ArchivedPage < Phlex::HTML
|
|
21
|
+
include DocsUI
|
|
22
|
+
|
|
23
|
+
def initialize(entry: nil)
|
|
24
|
+
@entry = entry
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def view_template
|
|
28
|
+
render DocsUI::Shell.new(title: @entry&.title) { body }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The masthead + Markdown body — separated from the Shell wrapper so it can
|
|
32
|
+
# render (and be specced) without a Rails view context, the same seam as
|
|
33
|
+
# Shell's own topbar/theme-script specs.
|
|
34
|
+
def body
|
|
35
|
+
render DocsUI::Header.new(@entry.title) if @entry&.title
|
|
36
|
+
render DocsUI::Markdown.new(markdown_source) unless markdown_source.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def markdown_source
|
|
42
|
+
@markdown_source ||= @entry ? @entry.markdown.to_s : ""
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -19,7 +19,7 @@ module DocsUI
|
|
|
19
19
|
# token => official Simple-Icons path data (viewBox 0 0 24 24, fill rule
|
|
20
20
|
# nonzero). Sourced verbatim from simpleicons.org; each is a single <path d>.
|
|
21
21
|
# The path strings are inherently one long line each — never reflow them.
|
|
22
|
-
# rubocop:disable Layout/LineLength
|
|
22
|
+
# rubocop:disable-next Layout/LineLength
|
|
23
23
|
BRANDS = {
|
|
24
24
|
github: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
|
|
25
25
|
gitlab: "m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z",
|
|
@@ -36,7 +36,6 @@ module DocsUI
|
|
|
36
36
|
reddit: "M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z",
|
|
37
37
|
stackoverflow: "M15.725 0l-1.72 1.277 6.39 8.588 1.716-1.277L15.725 0zm-3.94 3.418l-1.369 1.644 8.225 6.85 1.369-1.644-8.225-6.85zm-3.15 4.465l-.905 1.94 9.702 4.517.904-1.94-9.701-4.517zm-1.85 4.86l-.44 2.093 10.473 2.201.44-2.092-10.473-2.203zM1.89 15.47V24h19.19v-8.53h-2.133v6.397H4.021v-6.396H1.89zm4.265 2.133v2.13h10.66v-2.13H6.154Z"
|
|
38
38
|
}.freeze
|
|
39
|
-
# rubocop:enable Layout/LineLength
|
|
40
39
|
|
|
41
40
|
# True when `token` names a shipped brand mark (symbol or string), false for a
|
|
42
41
|
# lucide name, nil, or a blank string. The Shell uses this to decide whether a
|
|
@@ -29,6 +29,11 @@ module DocsKit
|
|
|
29
29
|
# GET-only, sessionless, public text endpoints (no token to verify).
|
|
30
30
|
protect_from_forgery with: :null_session
|
|
31
31
|
|
|
32
|
+
# Every action runs in the request's version scope (params[:version] on the
|
|
33
|
+
# version-prefixed routes, else the current version), so the enumeration
|
|
34
|
+
# below serves the version the URL asked for.
|
|
35
|
+
include DocsKit::Scoping
|
|
36
|
+
|
|
32
37
|
def index
|
|
33
38
|
body = DocsKit::LlmsText.index(docs_config, base_url: request.base_url)
|
|
34
39
|
render_text(body) if stale_llms?(body)
|
|
@@ -67,9 +72,11 @@ module DocsKit
|
|
|
67
72
|
# A page's Markdown twin, rendered through this controller's view context so
|
|
68
73
|
# url helpers/CSRF resolve and relative links absolutize to portable URLs —
|
|
69
74
|
# the same path DocsKit::Controller#render_page takes for a `.md` request.
|
|
75
|
+
# renderable_for is the live-or-snapshot shim (a snapshot entry renders an
|
|
76
|
+
# ArchivedPage carrying its frozen Markdown).
|
|
70
77
|
def render_page_markdown(page)
|
|
71
78
|
DocsKit::MarkdownExport.new(
|
|
72
|
-
page
|
|
79
|
+
DocsKit::LlmsText.renderable_for(page), view_context:, base_url: request.base_url
|
|
73
80
|
).to_md
|
|
74
81
|
end
|
|
75
82
|
end
|
|
@@ -15,7 +15,7 @@ module DocsKit
|
|
|
15
15
|
# DocsKit::McpServer / DocsKit::McpTools.
|
|
16
16
|
#
|
|
17
17
|
# Stateless JSON-RPC: each POST is independent (no SSE session), so it works
|
|
18
|
-
# behind the existing
|
|
18
|
+
# behind the existing dash/Cloudflare deploy unchanged. #create delegates the
|
|
19
19
|
# whole protocol to DocsKit::McpServer#handle_json — the SDK parses the request,
|
|
20
20
|
# dispatches the tool, and serializes the response (including JSON-RPC errors),
|
|
21
21
|
# so the controller never hand-rolls the protocol.
|
|
@@ -31,6 +31,10 @@ module DocsKit
|
|
|
31
31
|
# protection outright.
|
|
32
32
|
skip_forgery_protection
|
|
33
33
|
|
|
34
|
+
# MCP tool calls run in the request's version scope; the version-aware tool
|
|
35
|
+
# arguments (issue #61 phase 6) layer per-call resolution on top of this.
|
|
36
|
+
include DocsKit::Scoping
|
|
37
|
+
|
|
34
38
|
def create
|
|
35
39
|
return head(:not_found) unless docs_config.mcp_enabled?
|
|
36
40
|
|
|
@@ -28,6 +28,11 @@ module DocsKit
|
|
|
28
28
|
# public endpoint.
|
|
29
29
|
protect_from_forgery with: :null_session
|
|
30
30
|
|
|
31
|
+
# Search follows the request's version: /1.0/docs/search searches the 1.0
|
|
32
|
+
# snapshot, /docs/search searches current — the scope swaps the enumeration
|
|
33
|
+
# source underneath DocsKit::LlmsText.pages.
|
|
34
|
+
include DocsKit::Scoping
|
|
35
|
+
|
|
31
36
|
def index
|
|
32
37
|
hits = search_index.search(query)
|
|
33
38
|
|
|
@@ -53,7 +58,7 @@ module DocsKit
|
|
|
53
58
|
def search_index
|
|
54
59
|
triples = DocsKit::LlmsText.pages(docs_config).map do |page|
|
|
55
60
|
markdown = DocsKit::MarkdownExport.new(
|
|
56
|
-
page
|
|
61
|
+
DocsKit::LlmsText.renderable_for(page), view_context:, base_url: request.base_url
|
|
57
62
|
).to_md
|
|
58
63
|
[page.title, page.href, markdown]
|
|
59
64
|
end
|
data/exe/docs-kit
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
# docs-kit CLI. One command scaffolds a complete, deployable docs site:
|
|
5
5
|
#
|
|
6
6
|
# docs-kit new my-docs
|
|
7
|
-
# docs-kit new my-docs --image
|
|
7
|
+
# docs-kit new my-docs --image zoolutions/my-repo --service my-repo
|
|
8
8
|
#
|
|
9
9
|
# It runs `rails new` with the right minimal flags and applies docs-kit's
|
|
10
10
|
# application template (lib/docs_kit/templates/new_site.rb), which adds the gem,
|
|
11
|
-
# runs `docs_kit:install`, syncs icons, builds the CSS, and scaffolds
|
|
11
|
+
# runs `docs_kit:install`, syncs icons, builds the CSS, and scaffolds dash +
|
|
12
12
|
# the reusable deploy workflow.
|
|
13
13
|
|
|
14
14
|
require "optparse"
|
|
@@ -25,9 +25,9 @@ unless command == "new"
|
|
|
25
25
|
docs-kit new NAME [--image OWNER/REPO] [--service NAME] [--gem-source SRC]
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
|
-
--image GHCR image (default
|
|
28
|
+
--image GHCR image (default zoolutions/NAME) — use OWNER/REPO for the
|
|
29
29
|
repo-linked package so GITHUB_TOKEN can push+pull it.
|
|
30
|
-
--service
|
|
30
|
+
--service dash service name (default NAME).
|
|
31
31
|
--gem-source How to depend on docs-kit: 'released' (default),
|
|
32
32
|
'path:PATH', or 'github:OWNER/REPO'.
|
|
33
33
|
USAGE
|
|
@@ -58,7 +58,7 @@ gem_source =
|
|
|
58
58
|
|
|
59
59
|
env = {
|
|
60
60
|
"DOCS_KIT_GEM_SOURCE" => gem_source,
|
|
61
|
-
"DOCS_KIT_IMAGE" => opts[:image] || "
|
|
61
|
+
"DOCS_KIT_IMAGE" => opts[:image] || "zoolutions/#{name}",
|
|
62
62
|
"DOCS_KIT_SERVICE" => opts[:service] || name
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -242,6 +242,27 @@ module DocsKit
|
|
|
242
242
|
# #openapi_document (which memoizes + reloads on file change), never @openapi.
|
|
243
243
|
attr_accessor :openapi
|
|
244
244
|
|
|
245
|
+
# The documentation versions this site serves — a list of Hashes
|
|
246
|
+
# ({ id:, label:, ref:, current:, noindex: }) or DocsKit::DocVersion objects;
|
|
247
|
+
# #versions normalizes them. Defaults to [] → versioning is off and the site
|
|
248
|
+
# is byte-identical to before. The `current` entry keeps serving unprefixed
|
|
249
|
+
# at /docs; every other entry serves a committed Markdown snapshot at
|
|
250
|
+
# /<id>/docs (see DocsKit::Snapshot). A version id must match v?\d+(\.\d+)*
|
|
251
|
+
# so the host's static version route constraint recognizes it. Read via
|
|
252
|
+
# #versions, never @versions.
|
|
253
|
+
attr_writer :versions
|
|
254
|
+
|
|
255
|
+
# The site's source repository root (e.g. "https://github.com/me/repo"),
|
|
256
|
+
# used for the GitHub compare link between two versions' refs
|
|
257
|
+
# (#compare_url). Defaults to nil → no compare link renders.
|
|
258
|
+
attr_accessor :repo_url
|
|
259
|
+
|
|
260
|
+
# Where committed version snapshots live. Defaults to nil, which the reader
|
|
261
|
+
# resolves to Rails.root/"docs_snapshots" under Rails (nil outside Rails —
|
|
262
|
+
# the standalone suite points at fixtures explicitly). Read via
|
|
263
|
+
# #snapshots_path, never @snapshots_path.
|
|
264
|
+
attr_writer :snapshots_path
|
|
265
|
+
|
|
245
266
|
# The sentinel "no explicit nav" lambda. #nav_groups compares against this
|
|
246
267
|
# identity to decide whether to derive the sidebar from #nav_registries.
|
|
247
268
|
DEFAULT_NAV = -> { {} }
|
|
@@ -310,6 +331,9 @@ module DocsKit
|
|
|
310
331
|
@brand_logo = nil
|
|
311
332
|
@brand_logo_raw = nil
|
|
312
333
|
@topbar_brand = :always
|
|
334
|
+
@versions = []
|
|
335
|
+
@repo_url = nil
|
|
336
|
+
@snapshots_path = nil
|
|
313
337
|
end
|
|
314
338
|
|
|
315
339
|
# The normalized App Home link (a DocsKit::TopbarLink), or nil when unset —
|
|
@@ -338,6 +362,61 @@ module DocsKit
|
|
|
338
362
|
Array(@topbar_links).map { |link| DocsKit::TopbarLink.from(link) }
|
|
339
363
|
end
|
|
340
364
|
|
|
365
|
+
# The normalized version list (DocsKit::DocVersion list), in declaration
|
|
366
|
+
# order. Each configured Hash/DocVersion is coerced via DocVersion.from, so
|
|
367
|
+
# the switcher and the snapshot reader only ever see value objects.
|
|
368
|
+
# Blank/nil config yields [].
|
|
369
|
+
def versions
|
|
370
|
+
Array(@versions).map { |version| DocsKit::DocVersion.from(version) }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# The version serving unprefixed at /docs: the entry marked current: true,
|
|
374
|
+
# else the first configured entry, else nil (an unversioned site).
|
|
375
|
+
def current_version
|
|
376
|
+
versions.find(&:current?) || versions.first
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# The configured version with this id, or nil when unknown (or nil id).
|
|
380
|
+
def version(id)
|
|
381
|
+
return if id.nil?
|
|
382
|
+
|
|
383
|
+
versions.find { |version| version.id.to_s == id.to_s }
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# The version a request's :version param resolves to: the strict #version
|
|
387
|
+
# lookup, falling back to #current_version for an unknown or missing id —
|
|
388
|
+
# one rule shared by DocsKit::Controller#render_page and the gem's own
|
|
389
|
+
# controllers (DocsKit::Scoping), so a bad param degrades to the current
|
|
390
|
+
# docs instead of 500ing.
|
|
391
|
+
def resolve_version(id)
|
|
392
|
+
version(id) || current_version
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Whether the version chrome (switcher, llms.txt Versions block) renders.
|
|
396
|
+
# A single configured version is not worth a switcher, so this needs two —
|
|
397
|
+
# and an unconfigured site stays byte-identical to before.
|
|
398
|
+
def versioning_enabled?
|
|
399
|
+
versions.size > 1
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# The resolved snapshots directory: the configured value verbatim, else
|
|
403
|
+
# Rails.root/"docs_snapshots" under Rails, else nil (no Rails, no default —
|
|
404
|
+
# the standalone suite passes explicit paths).
|
|
405
|
+
def snapshots_path
|
|
406
|
+
return @snapshots_path if @snapshots_path
|
|
407
|
+
|
|
408
|
+
Rails.root.join("docs_snapshots") if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# The GitHub compare URL between two versions' refs
|
|
412
|
+
# ("{repo_url}/compare/{from.ref}...{to.ref}"), or nil unless #repo_url and
|
|
413
|
+
# BOTH refs are present — absent value, absent link, never a broken one.
|
|
414
|
+
def compare_url(from, to)
|
|
415
|
+
return if repo_url.nil? || from&.ref.nil? || to&.ref.nil?
|
|
416
|
+
|
|
417
|
+
"#{repo_url.chomp('/')}/compare/#{from.ref}...#{to.ref}"
|
|
418
|
+
end
|
|
419
|
+
|
|
341
420
|
# The SEO / social-share knobs (DocsKit::SeoConfig), read by DocsUI::MetaTags.
|
|
342
421
|
# Lazily built and memoized so a `c.seo.description = ...` block mutates the
|
|
343
422
|
# one instance the Shell later reads. A site that never touches it gets the
|
|
@@ -502,11 +581,17 @@ module DocsKit
|
|
|
502
581
|
|
|
503
582
|
# The resolved nav Hash for this request. Always returns a Hash.
|
|
504
583
|
#
|
|
505
|
-
# An
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
#
|
|
584
|
+
# An ARCHIVED version in DocsKit::Scope wins outright: the sidebar derives
|
|
585
|
+
# from that version's snapshot manifest (hrefs already version-prefixed), so
|
|
586
|
+
# an archived page never links into the live docs — even a site's explicit
|
|
587
|
+
# #nav lambda describes the live pages, not the frozen ones. With no scope
|
|
588
|
+
# (or the current version) nothing changes: an explicit #nav lambda wins,
|
|
589
|
+
# else the sidebar derives from #nav_registries — each heading maps to its
|
|
590
|
+
# registry's .nav_items, and a heading whose pages are all unauthored
|
|
591
|
+
# (empty nav_items) is dropped so no empty group renders.
|
|
509
592
|
def nav_groups
|
|
593
|
+
scope_version = DocsKit::Scope.version
|
|
594
|
+
return DocsKit::Snapshot.for(scope_version, config: self).nav_groups if scope_version&.archived?
|
|
510
595
|
return nav_groups_from_registries unless @nav_explicit
|
|
511
596
|
|
|
512
597
|
result = @nav.respond_to?(:call) ? @nav.call : @nav
|
data/lib/docs_kit/controller.rb
CHANGED
|
@@ -19,10 +19,19 @@ module DocsKit
|
|
|
19
19
|
# from the SAME render (DocsKit::MarkdownExport walks the rendered HTML). So
|
|
20
20
|
# `GET /docs/x.md` is faithful GFM of exactly what `/docs/x` shows — the
|
|
21
21
|
# author writes nothing extra, and the two never drift.
|
|
22
|
+
#
|
|
23
|
+
# The render runs inside the request's DocsKit::Scope (the version resolved
|
|
24
|
+
# from params[:version], falling back to the current version), so the
|
|
25
|
+
# sidebar/meta tags/enumeration all see the version the URL asked for.
|
|
26
|
+
# `render` renders synchronously inside the action, so this block wrapper is
|
|
27
|
+
# sufficient — no around_action, no host code changes. On an unversioned
|
|
28
|
+
# site the scope is nil: today's behavior exactly.
|
|
22
29
|
def render_page(view)
|
|
23
|
-
|
|
30
|
+
DocsKit::Scope.with(version: DocsKit.configuration.resolve_version(params[:version])) do
|
|
31
|
+
return render_markdown(view) if markdown_request?
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
render view, layout: false
|
|
34
|
+
end
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
private
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
# One documentation version a site serves. Sites declare these in config as
|
|
5
|
+
# plain Hashes; #versions normalizes each into a DocVersion so the chrome and
|
|
6
|
+
# the AI surfaces stay value-object-driven (like DocsKit::TopbarLink):
|
|
7
|
+
#
|
|
8
|
+
# c.versions = [
|
|
9
|
+
# { id: "1.1", ref: "v1.1.0", current: true },
|
|
10
|
+
# { id: "1.0", ref: "v1.0.0" },
|
|
11
|
+
# ]
|
|
12
|
+
#
|
|
13
|
+
# #id is the URL segment (an archived version serves at "/#{id}/docs/...");
|
|
14
|
+
# #label is the switcher text (defaults to the id); #ref is the git ref backing
|
|
15
|
+
# the GitHub compare link (optional); #current marks the version serving
|
|
16
|
+
# unprefixed at /docs (exactly today's URLs); #noindex defaults to the inverse
|
|
17
|
+
# of #current — archived copies are noindex'd so search engines keep pointing
|
|
18
|
+
# at the current docs, overridable per version with `noindex: false`.
|
|
19
|
+
#
|
|
20
|
+
# Named DocVersion, not Version — lib/docs_kit/version.rb already owns that
|
|
21
|
+
# file slot and defines DocsKit::VERSION.
|
|
22
|
+
DocVersion = Data.define(:id, :label, :ref, :current, :noindex) do
|
|
23
|
+
def initialize(id:, label: nil, ref: nil, current: false, noindex: nil)
|
|
24
|
+
super(
|
|
25
|
+
id: id,
|
|
26
|
+
label: label || id.to_s,
|
|
27
|
+
ref: ref,
|
|
28
|
+
current: current,
|
|
29
|
+
noindex: noindex.nil? ? !current : noindex
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Build a DocVersion from a Hash (symbol- OR string-keyed, so a YAML/JSON
|
|
34
|
+
# config loads cleanly) or pass an existing DocVersion through unchanged.
|
|
35
|
+
def self.from(version)
|
|
36
|
+
return version if version.is_a?(self)
|
|
37
|
+
|
|
38
|
+
attrs = version.to_h.transform_keys(&:to_sym)
|
|
39
|
+
new(
|
|
40
|
+
id: attrs[:id],
|
|
41
|
+
label: attrs[:label],
|
|
42
|
+
ref: attrs[:ref],
|
|
43
|
+
current: attrs.fetch(:current, false),
|
|
44
|
+
noindex: attrs[:noindex]
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def current? = !!current
|
|
49
|
+
|
|
50
|
+
def archived? = !current?
|
|
51
|
+
|
|
52
|
+
# The root URL segment this version contributes: "" for the current version
|
|
53
|
+
# (existing sites and their SEO untouched), "/#{id}" for an archived one.
|
|
54
|
+
# Stacks with the i18n locale prefix later ("/de/1.0/docs/...").
|
|
55
|
+
def path_prefix
|
|
56
|
+
current? ? "" : "/#{id}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/docs_kit/llms_text.rb
CHANGED
|
@@ -54,13 +54,40 @@ module DocsKit
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# The authored pages
|
|
58
|
-
# responds to #title / #href / #view_class
|
|
59
|
-
#
|
|
60
|
-
|
|
57
|
+
# The authored pages for one version of the docs, in config/registry order —
|
|
58
|
+
# each responds to #title / #href / #view_class (render via .renderable_for).
|
|
59
|
+
# This is the ONE enumeration seam every AI surface funnels through, so
|
|
60
|
+
# making IT version-aware makes llms-full.txt, search, and MCP follow the
|
|
61
|
+
# request's version for free.
|
|
62
|
+
#
|
|
63
|
+
# version: nil resolves through DocsKit::Scope (set per request by the
|
|
64
|
+
# controllers), then config.current_version — so an unversioned site, or the
|
|
65
|
+
# current version, enumerates the live registries exactly as before. An
|
|
66
|
+
# ARCHIVED version enumerates its Markdown snapshot instead
|
|
67
|
+
# (DocsKit::Snapshot — every entry is authored by definition).
|
|
68
|
+
def pages(config, version: nil)
|
|
69
|
+
version ||= DocsKit::Scope.version || config.current_version
|
|
70
|
+
return snapshot_pages(config, version) if version&.archived?
|
|
71
|
+
|
|
61
72
|
config.nav_registries.values.flat_map { |registry| registry.all.select(&:view_class) }
|
|
62
73
|
end
|
|
63
74
|
|
|
75
|
+
# An archived version's pages, from its committed snapshot. Every entry has
|
|
76
|
+
# a view_class by construction; the select keeps the authored-pages contract
|
|
77
|
+
# symmetric with the live branch.
|
|
78
|
+
def snapshot_pages(config, version)
|
|
79
|
+
DocsKit::Snapshot.for(version, config: config).all.select(&:view_class)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The Phlex renderable for a page returned by .pages: the page's own
|
|
83
|
+
# #renderable (Registry v2 Entry, Snapshot::Entry) with a backwards-
|
|
84
|
+
# compatible fallback to view_class.new for a site's custom `entries`-style
|
|
85
|
+
# registry class that predates #renderable. The ONE shim — the controllers
|
|
86
|
+
# and MCP tools all call this rather than repeating the respond_to? check.
|
|
87
|
+
def renderable_for(page)
|
|
88
|
+
page.respond_to?(:renderable) ? page.renderable : page.view_class.new
|
|
89
|
+
end
|
|
90
|
+
|
|
64
91
|
# The llms-full.txt body: each [title, markdown] pair as `# {title}` + body,
|
|
65
92
|
# separated by a `---` rule. Empty pairs → "".
|
|
66
93
|
def full(_config, title_markdown_pairs)
|
|
@@ -20,8 +20,9 @@ module DocsKit
|
|
|
20
20
|
# yield nothing (whitespace-only text nodes) are dropped so no stray blank
|
|
21
21
|
# lines accumulate.
|
|
22
22
|
def render(node)
|
|
23
|
-
node.children
|
|
24
|
-
|
|
23
|
+
node.children
|
|
24
|
+
.filter_map { |child| block(child) }
|
|
25
|
+
.reject(&:empty?)
|
|
25
26
|
.join("\n\n")
|
|
26
27
|
end
|
|
27
28
|
|
data/lib/docs_kit/mcp_tools.rb
CHANGED
|
@@ -87,8 +87,9 @@ module DocsKit
|
|
|
87
87
|
|
|
88
88
|
# A page's GFM Markdown twin, rendered through the view context so url helpers
|
|
89
89
|
# and relative-link absolutization resolve — the LlmsController#full seam.
|
|
90
|
+
# renderable_for is the live-or-snapshot shim (see LlmsText.renderable_for).
|
|
90
91
|
def render_markdown(page, base_url:, view_context:)
|
|
91
|
-
MarkdownExport.new(page
|
|
92
|
+
MarkdownExport.new(LlmsText.renderable_for(page), view_context:, base_url:).to_md
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
# A DocsKit::SearchIndex over every authored page's twin — the same triples
|
data/lib/docs_kit/registry.rb
CHANGED
|
@@ -147,6 +147,13 @@ module DocsKit
|
|
|
147
147
|
|
|
148
148
|
"#{@view_namespace}::#{@view_name}".safe_constantize
|
|
149
149
|
end
|
|
150
|
+
|
|
151
|
+
# The renderable instance for this page (nil when unauthored) — the seam
|
|
152
|
+
# DocsKit::Snapshot::Entry shares, so consumers render live pages and
|
|
153
|
+
# snapshot pages identically (see LlmsText.renderable_for).
|
|
154
|
+
def renderable
|
|
155
|
+
view_class&.new
|
|
156
|
+
end
|
|
150
157
|
end
|
|
151
158
|
end
|
|
152
159
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
# The ONE request-scoped content scope: which documentation version (and,
|
|
5
|
+
# come i18n M2, which locale) the current render serves. Controllers set it
|
|
6
|
+
# around an action (DocsKit::Controller#render_page, DocsKit::Scoping); the
|
|
7
|
+
# config and the components consult it (Configuration#nav_groups,
|
|
8
|
+
# LlmsText.pages) — so "which content tree?" is asked once per request, not
|
|
9
|
+
# threaded through every component.
|
|
10
|
+
#
|
|
11
|
+
# DocsKit::Scope.with(version: v) { ... } # block-scoped, restores in an ensure
|
|
12
|
+
# DocsKit::Scope.version # the DocVersion in scope, or nil
|
|
13
|
+
# DocsKit::Scope.locale # reserved for i18n M2 — nil today
|
|
14
|
+
# DocsKit::Scope.path_prefix # "" or "/1.0"
|
|
15
|
+
#
|
|
16
|
+
# Backed by Thread.current[] — fiber-local in Ruby, which is what a fibered
|
|
17
|
+
# server wants — and deliberately Rails-free (NOT CurrentAttributes), so bare
|
|
18
|
+
# Phlex component specs can set a scope without booting Rails. An empty scope
|
|
19
|
+
# (no `with` in flight) reads as nil version / nil locale, which every
|
|
20
|
+
# consumer treats as "the current version" — today's behavior exactly.
|
|
21
|
+
module Scope
|
|
22
|
+
KEY = :docs_kit_scope
|
|
23
|
+
|
|
24
|
+
EMPTY = { version: nil, locale: nil }.freeze
|
|
25
|
+
private_constant :EMPTY
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Run the block with this version/locale in scope, restoring the previous
|
|
30
|
+
# scope on the way out — even when the block raises — so nothing leaks
|
|
31
|
+
# across requests sharing a thread.
|
|
32
|
+
def with(version: nil, locale: nil)
|
|
33
|
+
previous = Thread.current[KEY]
|
|
34
|
+
Thread.current[KEY] = { version: version, locale: locale }
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
Thread.current[KEY] = previous
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The DocsKit::DocVersion in scope, or nil (treated as the current version).
|
|
41
|
+
def version
|
|
42
|
+
current[:version]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Reserved for i18n M2 — always nil until the locale axis is wired.
|
|
46
|
+
def locale
|
|
47
|
+
current[:locale]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The root URL prefix the in-scope version contributes ("" when none/current).
|
|
51
|
+
def path_prefix
|
|
52
|
+
version&.path_prefix || ""
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def current
|
|
56
|
+
Thread.current[KEY] || EMPTY
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
# Wraps a controller's actions in the request's DocsKit::Scope, so everything
|
|
5
|
+
# rendered or enumerated during the action (Configuration#nav_groups,
|
|
6
|
+
# LlmsText.pages, the search index) sees the same version the URL asked for.
|
|
7
|
+
# Included by the gem's own controllers (Llms, Search, Mcp); a host's docs
|
|
8
|
+
# controller gets the same behavior from DocsKit::Controller#render_page's own
|
|
9
|
+
# wrapper instead — including this module there would around_action every host
|
|
10
|
+
# action, which is not docs-kit's call to make.
|
|
11
|
+
#
|
|
12
|
+
# A plain module with an included hook, not an ActiveSupport::Concern — it has
|
|
13
|
+
# no dependency chain and stays loadable in the Rails-free suite.
|
|
14
|
+
module Scoping
|
|
15
|
+
def self.included(base)
|
|
16
|
+
base.around_action :docs_scope
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# The requested version (params[:version], falling back to the current
|
|
22
|
+
# version — an unknown id degrades, never 500s) held in scope for the whole
|
|
23
|
+
# action. nil on an unversioned site: today's behavior exactly.
|
|
24
|
+
def docs_scope(&)
|
|
25
|
+
DocsKit::Scope.with(version: DocsKit.configuration.resolve_version(params[:version]), &)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsKit
|
|
4
|
+
class Snapshot
|
|
5
|
+
# One snapshot page — the duck type of DocsKit::Registry::Entry (#slug /
|
|
6
|
+
# #title / #group / #icon / #href / #view_class / #renderable), so the
|
|
7
|
+
# enumeration seam (LlmsText.pages) and its consumers treat a frozen
|
|
8
|
+
# Markdown page exactly like a live Ruby one. #view_class is the truthy
|
|
9
|
+
# DocsUI::ArchivedPage constant, so the `select(&:view_class)` authored-page
|
|
10
|
+
# filter passes unchanged.
|
|
11
|
+
class Entry
|
|
12
|
+
attr_reader :slug, :title, :group, :icon, :file, :digest, :href
|
|
13
|
+
|
|
14
|
+
def initialize(attrs, version:, root:, registry_prefix:)
|
|
15
|
+
@slug = attrs["slug"]
|
|
16
|
+
@title = attrs["title"]
|
|
17
|
+
@group = attrs["group"]
|
|
18
|
+
@icon = attrs["icon"]
|
|
19
|
+
@file = attrs["file"]
|
|
20
|
+
@digest = attrs["digest"]
|
|
21
|
+
@root = root
|
|
22
|
+
@href = "#{version.path_prefix}#{registry_prefix}/#{@slug}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The renderer for every archived page. Truthy (never nil): a snapshot
|
|
26
|
+
# page is by definition authored — its content is the committed .md file.
|
|
27
|
+
def view_class
|
|
28
|
+
DocsUI::ArchivedPage
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The renderable the controllers hand to Phlex — an ArchivedPage carrying
|
|
32
|
+
# this entry, where a live Registry::Entry builds `view_class.new`.
|
|
33
|
+
def renderable
|
|
34
|
+
DocsUI::ArchivedPage.new(entry: self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The raw Markdown body from the snapshot file. A missing/unreadable file
|
|
38
|
+
# degrades to "" — the page renders empty rather than 500ing.
|
|
39
|
+
def markdown
|
|
40
|
+
return "" if @root.nil? || @file.nil?
|
|
41
|
+
|
|
42
|
+
@root.join(@file).read
|
|
43
|
+
rescue SystemCallError
|
|
44
|
+
""
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module DocsKit
|
|
7
|
+
# Reads a committed Markdown snapshot of one documentation version back as the
|
|
8
|
+
# registry duck type the rest of the kit already speaks (#all / #from_slug /
|
|
9
|
+
# #nav_items), so an archived version renders through TODAY's chrome — only
|
|
10
|
+
# the content is frozen.
|
|
11
|
+
#
|
|
12
|
+
# A snapshot lives at <config.snapshots_path>/<version id>/: a manifest.json
|
|
13
|
+
# describing the nav structure (see the schema in the snapshot task) plus one
|
|
14
|
+
# .md file per page, written by the host-run `bin/rails docs_kit:snapshot[id]`
|
|
15
|
+
# task at release time.
|
|
16
|
+
#
|
|
17
|
+
# A missing directory or unreadable manifest degrades to an EMPTY snapshot
|
|
18
|
+
# (no pages) — a version configured before its snapshot is written must never
|
|
19
|
+
# take the site down. The install generator's --sync report warns about the
|
|
20
|
+
# drift instead.
|
|
21
|
+
class Snapshot
|
|
22
|
+
# The manifest format this reader understands; the writer stamps it so a
|
|
23
|
+
# future format change is detectable rather than silently misread.
|
|
24
|
+
SCHEMA = 1
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# The snapshot for this version, memoized per [version id, directory] and
|
|
28
|
+
# invalidated when manifest.json's mtime changes — the same
|
|
29
|
+
# reload-on-change posture as Configuration#openapi_document, so editing
|
|
30
|
+
# a snapshot in development is picked up without a server restart.
|
|
31
|
+
def for(version, config: DocsKit.configuration)
|
|
32
|
+
version = DocVersion.from(version)
|
|
33
|
+
root = root_for(version, config)
|
|
34
|
+
mtime = manifest_mtime(root)
|
|
35
|
+
key = [version.id.to_s, root.to_s]
|
|
36
|
+
|
|
37
|
+
@cache ||= {}
|
|
38
|
+
cached = @cache[key]
|
|
39
|
+
return cached.fetch(:snapshot) if cached && cached.fetch(:mtime) == mtime
|
|
40
|
+
|
|
41
|
+
new(version: version, root: root).tap do |snapshot|
|
|
42
|
+
@cache[key] = { snapshot: snapshot, mtime: mtime }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reset_cache!
|
|
47
|
+
@cache = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# <snapshots_path>/<id>, or nil when no snapshots path resolves (no
|
|
53
|
+
# config, no Rails) — which reads back as an empty snapshot.
|
|
54
|
+
def root_for(version, config)
|
|
55
|
+
base = config.snapshots_path
|
|
56
|
+
return if base.nil?
|
|
57
|
+
|
|
58
|
+
Pathname.new(base).join(version.id.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def manifest_mtime(root)
|
|
62
|
+
return if root.nil?
|
|
63
|
+
|
|
64
|
+
path = root.join("manifest.json")
|
|
65
|
+
path.file? ? path.mtime : nil
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
attr_reader :version, :root
|
|
72
|
+
|
|
73
|
+
def initialize(version:, root:)
|
|
74
|
+
@version = version
|
|
75
|
+
@root = root
|
|
76
|
+
@manifest = read_manifest
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Every snapshot page across the manifest's registries, in manifest order —
|
|
80
|
+
# each a Snapshot::Entry quacking like a Registry::Entry.
|
|
81
|
+
def all
|
|
82
|
+
registries.flat_map { |registry| registry.fetch(:entries) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def from_slug(slug)
|
|
86
|
+
all.find { |entry| entry.slug.to_s == slug.to_s }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# { group => [NavItem] }, the Registry.nav_items shape — hrefs already carry
|
|
90
|
+
# the version prefix, so the Sidebar's strict path == href active-matching
|
|
91
|
+
# works unchanged.
|
|
92
|
+
def nav_items
|
|
93
|
+
nav_items_for(all)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# { heading => { group => [NavItem] } }, the Configuration#nav_groups shape,
|
|
97
|
+
# from the manifest's per-registry headings — a heading with no pages is
|
|
98
|
+
# dropped so the sidebar never shows an empty group.
|
|
99
|
+
def nav_groups
|
|
100
|
+
registries.each_with_object({}) do |registry, acc|
|
|
101
|
+
items = nav_items_for(registry.fetch(:entries))
|
|
102
|
+
acc[registry.fetch(:heading)] = items unless items.empty?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The version-prefixed docs prefix (e.g. "/1.0/docs").
|
|
107
|
+
def path_prefix
|
|
108
|
+
"#{version.path_prefix}/docs"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The raw Markdown body of the page with this slug, or nil when unknown.
|
|
112
|
+
def markdown_for(slug)
|
|
113
|
+
from_slug(slug)&.markdown
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# The manifest's registries as { heading:, entries: [Snapshot::Entry] }.
|
|
119
|
+
def registries
|
|
120
|
+
@registries ||= Array(@manifest && @manifest["registries"]).map do |registry|
|
|
121
|
+
prefix = registry["path_prefix"] || "/docs"
|
|
122
|
+
{
|
|
123
|
+
heading: registry["heading"],
|
|
124
|
+
entries: Array(registry["pages"]).map do |attrs|
|
|
125
|
+
Entry.new(attrs, version: version, root: root, registry_prefix: prefix)
|
|
126
|
+
end
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def nav_items_for(entries)
|
|
132
|
+
entries.group_by(&:group).transform_values do |grouped|
|
|
133
|
+
grouped.map { |entry| NavItem.new(href: entry.href, label: entry.title, icon: entry.icon) }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The parsed manifest Hash, or nil (→ empty snapshot) when the directory or
|
|
138
|
+
# manifest is missing/unreadable — degrade, never raise (the site must stay
|
|
139
|
+
# up with a version configured before its snapshot exists).
|
|
140
|
+
def read_manifest
|
|
141
|
+
return if root.nil?
|
|
142
|
+
|
|
143
|
+
path = root.join("manifest.json")
|
|
144
|
+
return unless path.file?
|
|
145
|
+
|
|
146
|
+
JSON.parse(path.read)
|
|
147
|
+
rescue JSON::ParserError, SystemCallError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -14,16 +14,16 @@ require "securerandom"
|
|
|
14
14
|
# * adds docs-kit + its runtime deps to the Gemfile,
|
|
15
15
|
# * runs `docs_kit:install` (all the Ruby/CSS/Stimulus wiring),
|
|
16
16
|
# * syncs the lucide icon set and builds the CSS,
|
|
17
|
-
# * scaffolds a deployable
|
|
17
|
+
# * scaffolds a deployable dash setup that calls docs-kit's reusable workflow.
|
|
18
18
|
#
|
|
19
19
|
# The generated app is a complete, deployable standalone docs site.
|
|
20
20
|
|
|
21
21
|
# --- config the template reads ------------------------------------------------
|
|
22
22
|
# DOCS_KIT_GEM_SOURCE lets the dogfood site (docs-kit/docs) depend on the gem via
|
|
23
23
|
# path: ".." while a real new site depends on the released gem. Default: rubygems.
|
|
24
|
-
gem_source = ENV.fetch("DOCS_KIT_GEM_SOURCE", "") # e.g. 'path: "..", ' or 'github: "
|
|
24
|
+
gem_source = ENV.fetch("DOCS_KIT_GEM_SOURCE", "") # e.g. 'path: "..", ' or 'github: "zoolutions/docs-kit", '
|
|
25
25
|
# The GHCR image/service = the OWNER/REPO the site will live in (repo-linked pkg).
|
|
26
|
-
image = ENV.fetch("DOCS_KIT_IMAGE", "
|
|
26
|
+
image = ENV.fetch("DOCS_KIT_IMAGE", "zoolutions/#{app_name}")
|
|
27
27
|
service = ENV.fetch("DOCS_KIT_SERVICE", app_name)
|
|
28
28
|
|
|
29
29
|
# --- gems ---------------------------------------------------------------------
|
|
@@ -53,14 +53,26 @@ after_bundle do
|
|
|
53
53
|
run "bun install --silent" if system("command -v bun >/dev/null 2>&1")
|
|
54
54
|
run "bun run build:css" if system("command -v bun >/dev/null 2>&1")
|
|
55
55
|
|
|
56
|
-
# --- deploy scaffolding (
|
|
56
|
+
# --- deploy scaffolding (dash + the reusable workflow) ---------------------
|
|
57
57
|
create_file "config/deploy.yml", <<~YAML
|
|
58
|
-
#
|
|
58
|
+
# dash deploy → the oss-infrastructure server (Cloudflare Tunnel + dash-proxy).
|
|
59
59
|
# service/image = the repo OWNER/REPO so the ghcr package auto-links to the
|
|
60
60
|
# repo and GITHUB_TOKEN can push + pull it (no PAT). See docs-kit's README.
|
|
61
|
+
# `dash docs` / `dash docs proxy` is the always-current reference for every key.
|
|
61
62
|
service: #{service}
|
|
62
63
|
image: #{image}
|
|
63
64
|
|
|
65
|
+
# dash 4 renamed the on-host proxy (kamal-proxy → dash-proxy) and migrates a
|
|
66
|
+
# host in place; an older CLI must not deploy this config.
|
|
67
|
+
minimum_version: 4.0.0
|
|
68
|
+
|
|
69
|
+
# A stateless docs site never rolls back far — keep the host tidy.
|
|
70
|
+
retain_containers: 2
|
|
71
|
+
|
|
72
|
+
# Status-named pages (public/502.html, 503, 504) the proxy serves in place of
|
|
73
|
+
# the app's during a deploy gap — paired with `proxy.intercept_errors` below.
|
|
74
|
+
error_pages_path: public
|
|
75
|
+
|
|
64
76
|
servers:
|
|
65
77
|
web:
|
|
66
78
|
hosts:
|
|
@@ -72,17 +84,51 @@ after_bundle do
|
|
|
72
84
|
proxy:
|
|
73
85
|
host: <%= ENV["DEPLOY_DOMAIN"] %>
|
|
74
86
|
app_port: 3000
|
|
87
|
+
# TLS terminates at Cloudflare; the tunnel reaches the proxy over plain HTTP.
|
|
75
88
|
ssl: false
|
|
76
89
|
healthcheck:
|
|
77
90
|
path: /up
|
|
78
91
|
interval: 5
|
|
79
92
|
timeout: 30
|
|
80
93
|
|
|
94
|
+
# --- dash-proxy per-app features ------------------------------------------
|
|
95
|
+
# zstd / br / gzip negotiated at the edge; responses the app already encoded
|
|
96
|
+
# (Thruster) pass through untouched.
|
|
97
|
+
compress: true
|
|
98
|
+
|
|
99
|
+
# RFC 9111 shared cache. Only responses the app marks `Cache-Control: public,
|
|
100
|
+
# max-age` are stored (Propshaft assets, /llms*.txt) — HTML carrying a session
|
|
101
|
+
# cookie is refused by design. `dash proxy cache stats` shows what it holds.
|
|
102
|
+
cache:
|
|
103
|
+
enabled: true
|
|
104
|
+
max_ttl: 300
|
|
105
|
+
|
|
106
|
+
# Security headers set once here instead of per app; drop server fingerprints.
|
|
107
|
+
headers:
|
|
108
|
+
response:
|
|
109
|
+
set:
|
|
110
|
+
X-Content-Type-Options: nosniff
|
|
111
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
112
|
+
remove:
|
|
113
|
+
- Server
|
|
114
|
+
- X-Powered-By
|
|
115
|
+
|
|
116
|
+
# Serve public/<status>.html instead of a bare "Bad Gateway" while a
|
|
117
|
+
# container is swapped or unhealthy.
|
|
118
|
+
intercept_errors:
|
|
119
|
+
- 502
|
|
120
|
+
- 503
|
|
121
|
+
- 504
|
|
122
|
+
|
|
123
|
+
# Keep the health probe out of the request histograms.
|
|
124
|
+
exclude_metrics_paths:
|
|
125
|
+
- /up
|
|
126
|
+
|
|
81
127
|
registry:
|
|
82
128
|
server: ghcr.io
|
|
83
129
|
username: mhenrixon
|
|
84
130
|
password:
|
|
85
|
-
-
|
|
131
|
+
- DASH_REGISTRY_PASSWORD
|
|
86
132
|
|
|
87
133
|
builder:
|
|
88
134
|
arch: amd64
|
|
@@ -98,18 +144,24 @@ after_bundle do
|
|
|
98
144
|
SECRET_KEY_BASE: "#{SecureRandom.hex(64)}"
|
|
99
145
|
YAML
|
|
100
146
|
|
|
101
|
-
|
|
147
|
+
# The status pages `proxy.intercept_errors` serves for a deploy gap — `rails new`
|
|
148
|
+
# ships 500.html; the proxy looks for the exact status it intercepted.
|
|
149
|
+
%w[502 503 504].each do |status|
|
|
150
|
+
create_file "public/#{status}.html", File.read("public/500.html") if File.exist?("public/500.html")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
create_file ".dash/secrets", <<~SH
|
|
102
154
|
# In CI the deploy workflow sets this to the job's GITHUB_TOKEN. Locally,
|
|
103
|
-
# export it (e.g.
|
|
104
|
-
|
|
155
|
+
# export it (e.g. DASH_REGISTRY_PASSWORD=$(gh auth token)).
|
|
156
|
+
DASH_REGISTRY_PASSWORD=$DASH_REGISTRY_PASSWORD
|
|
105
157
|
SH
|
|
106
158
|
|
|
107
159
|
# The Dockerfile + .dockerignore are written by `docs_kit:install` (run above in
|
|
108
160
|
# after_bundle) so a scaffolded site and an upgrading site share ONE optimized,
|
|
109
161
|
# version-stamped Dockerfile — no divergent copy to maintain here. The generator
|
|
110
162
|
# derives the LABEL service from the app dir basename (= app_name); if the site
|
|
111
|
-
# deploys under a DIFFERENT
|
|
112
|
-
# match config/deploy.yml so
|
|
163
|
+
# deploys under a DIFFERENT dash service (`--service`), correct the label to
|
|
164
|
+
# match config/deploy.yml so dash's --skip-push validate_image passes.
|
|
113
165
|
gsub_file "Dockerfile", /LABEL service=".*"/, %(LABEL service="#{service}") if service != app_name
|
|
114
166
|
|
|
115
167
|
create_file ".github/workflows/deploy-docs.yml", <<~YAML
|
|
@@ -120,7 +172,7 @@ after_bundle do
|
|
|
120
172
|
workflow_dispatch:
|
|
121
173
|
jobs:
|
|
122
174
|
deploy:
|
|
123
|
-
uses:
|
|
175
|
+
uses: zoolutions/docs-kit/.github/workflows/deploy.yml@main
|
|
124
176
|
with:
|
|
125
177
|
image: #{image}
|
|
126
178
|
service: #{service}
|
data/lib/docs_kit/version.rb
CHANGED
|
@@ -568,7 +568,7 @@ module DocsKit
|
|
|
568
568
|
(name || File.basename(destination_root)).to_s.underscore.humanize
|
|
569
569
|
end
|
|
570
570
|
|
|
571
|
-
# The
|
|
571
|
+
# The dash `service` name stamped as the Dockerfile's LABEL — the app dir
|
|
572
572
|
# basename (a docs site's repo name), matching the `docs-kit new` default.
|
|
573
573
|
# Used in Dockerfile.tt via <%= docker_service %>.
|
|
574
574
|
def docker_service
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# `build` stage. Pair it with the shipped .dockerignore so the build context
|
|
8
8
|
# stays small (no node_modules, .git, logs, specs, coverage).
|
|
9
9
|
#
|
|
10
|
-
# Build context is the app root; `docker build .` (
|
|
10
|
+
# Build context is the app root; `docker build .` (dash: context: ".").
|
|
11
11
|
|
|
12
12
|
ARG RUBY_VERSION=<%= ruby_version_arg %>
|
|
13
13
|
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
|
|
@@ -59,9 +59,9 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile && \
|
|
|
59
59
|
# --- Final stage --------------------------------------------------------------
|
|
60
60
|
FROM base
|
|
61
61
|
|
|
62
|
-
#
|
|
62
|
+
# dash's validate_image greps this label on a --skip-push deploy; it must equal
|
|
63
63
|
# `service:` in config/deploy.yml. The reusable deploy workflow also stamps it,
|
|
64
|
-
# but keeping it here means `docker build` alone produces a
|
|
64
|
+
# but keeping it here means `docker build` alone produces a dash-valid image.
|
|
65
65
|
LABEL service="<%= docker_service %>"
|
|
66
66
|
|
|
67
67
|
# Copy ONLY the built artifacts from the build stage: the installed bundle and
|
|
@@ -79,9 +79,9 @@ EXPOSE 3000
|
|
|
79
79
|
<% if thruster? -%>
|
|
80
80
|
# Thruster fronts Puma (HTTP caching + compression + X-Sendfile). It listens on
|
|
81
81
|
# HTTP_PORT and proxies to Puma on TARGET_PORT (it sets PORT for the child, which
|
|
82
|
-
# config/puma.rb reads). HTTP_PORT MUST be the port traffic is routed to (
|
|
82
|
+
# config/puma.rb reads). HTTP_PORT MUST be the port traffic is routed to (dash's
|
|
83
83
|
# `app_port`, the EXPOSE above) — Thruster's default is 80, which the non-root
|
|
84
|
-
# user can't reliably bind AND which
|
|
84
|
+
# user can't reliably bind AND which dash-proxy (app_port: 3000) would never
|
|
85
85
|
# route to, silently bypassing Thruster straight into Puma.
|
|
86
86
|
ENV HTTP_PORT="3000" \
|
|
87
87
|
TARGET_PORT="3001"
|
|
@@ -8,7 +8,7 @@ reads it through the bundled `write-docs-page` skill. Edit freely — a
|
|
|
8
8
|
<!-- BEGIN docs-kit -->
|
|
9
9
|
## Writing docs pages (docs-kit)
|
|
10
10
|
|
|
11
|
-
<%= app_brand %> is a [docs-kit](https://github.com/
|
|
11
|
+
<%= app_brand %> is a [docs-kit](https://github.com/zoolutions/docs-kit) site: a
|
|
12
12
|
Phlex/daisyUI chrome where **every page is a `DocsUI::Page` subclass** and the
|
|
13
13
|
sidebar, TOC, search, and Markdown twin come free. To document something, you
|
|
14
14
|
scaffold a page, then write its `#content`. Never hand-write HTML or daisyUI
|
|
@@ -5,7 +5,7 @@ description: "Write, add, or update a documentation page in this docs-kit site.
|
|
|
5
5
|
|
|
6
6
|
# Write a docs page
|
|
7
7
|
|
|
8
|
-
This is a [docs-kit](https://github.com/
|
|
8
|
+
This is a [docs-kit](https://github.com/zoolutions/docs-kit) site (<%= app_brand %>).
|
|
9
9
|
Every page is a `DocsUI::Page` subclass; the shell, sidebar, "On this page" TOC,
|
|
10
10
|
search, and the `.md` twin all come free. Your job is to scaffold a page and
|
|
11
11
|
write its `#content` — never hand-write HTML or daisyUI markup.
|
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.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -164,6 +164,7 @@ files:
|
|
|
164
164
|
- CHANGELOG.md
|
|
165
165
|
- LICENSE.txt
|
|
166
166
|
- README.md
|
|
167
|
+
- app/components/docs_ui/archived_page.rb
|
|
167
168
|
- app/components/docs_ui/brand_mark.rb
|
|
168
169
|
- app/components/docs_ui/callout.rb
|
|
169
170
|
- app/components/docs_ui/code.rb
|
|
@@ -209,6 +210,7 @@ files:
|
|
|
209
210
|
- lib/docs_kit/brand_logo.rb
|
|
210
211
|
- lib/docs_kit/configuration.rb
|
|
211
212
|
- lib/docs_kit/controller.rb
|
|
213
|
+
- lib/docs_kit/doc_version.rb
|
|
212
214
|
- lib/docs_kit/engine.rb
|
|
213
215
|
- lib/docs_kit/landing_config.rb
|
|
214
216
|
- lib/docs_kit/llms_text.rb
|
|
@@ -226,11 +228,15 @@ files:
|
|
|
226
228
|
- lib/docs_kit/open_api/schema.rb
|
|
227
229
|
- lib/docs_kit/registry.rb
|
|
228
230
|
- lib/docs_kit/rubocop.rb
|
|
231
|
+
- lib/docs_kit/scope.rb
|
|
232
|
+
- lib/docs_kit/scoping.rb
|
|
229
233
|
- lib/docs_kit/search_hit.rb
|
|
230
234
|
- lib/docs_kit/search_index.rb
|
|
231
235
|
- lib/docs_kit/search_index/snippet.rb
|
|
232
236
|
- lib/docs_kit/seo_config.rb
|
|
233
237
|
- lib/docs_kit/shortcut.rb
|
|
238
|
+
- lib/docs_kit/snapshot.rb
|
|
239
|
+
- lib/docs_kit/snapshot/entry.rb
|
|
234
240
|
- lib/docs_kit/templates/new_site.rb
|
|
235
241
|
- lib/docs_kit/topbar_link.rb
|
|
236
242
|
- lib/docs_kit/version.rb
|
|
@@ -260,13 +266,13 @@ files:
|
|
|
260
266
|
- lib/generators/docs_kit/page/templates/page.rb.erb
|
|
261
267
|
- lib/rubocop/cop/docs_kit/escaped_interpolation_in_heredoc.rb
|
|
262
268
|
- lib/rubocop/cop/docs_kit/render_component_preferred.rb
|
|
263
|
-
homepage: https://github.com/
|
|
269
|
+
homepage: https://github.com/zoolutions/docs-kit
|
|
264
270
|
licenses:
|
|
265
271
|
- MIT
|
|
266
272
|
metadata:
|
|
267
|
-
source_code_uri: https://github.com/
|
|
268
|
-
changelog_uri: https://github.com/
|
|
269
|
-
bug_tracker_uri: https://github.com/
|
|
273
|
+
source_code_uri: https://github.com/zoolutions/docs-kit
|
|
274
|
+
changelog_uri: https://github.com/zoolutions/docs-kit/blob/main/CHANGELOG.md
|
|
275
|
+
bug_tracker_uri: https://github.com/zoolutions/docs-kit/issues
|
|
270
276
|
rubygems_mfa_required: 'true'
|
|
271
277
|
rdoc_options: []
|
|
272
278
|
require_paths:
|