journeybook 0.1.6.pre.alpha
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 +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +354 -0
- data/Rakefile +6 -0
- data/app/assets/images/journeybook/.keep +0 -0
- data/app/assets/images/journeybook/logo.svg +5 -0
- data/app/assets/javascripts/journeybook/sortable.js +96 -0
- data/app/assets/stylesheets/journeybook/application.css +1556 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/journeybook/public_pages.rb +31 -0
- data/app/controllers/journeybook/admin/ai_pages_controller.rb +65 -0
- data/app/controllers/journeybook/admin/ai_style_prompts_controller.rb +51 -0
- data/app/controllers/journeybook/admin/components_controller.rb +57 -0
- data/app/controllers/journeybook/admin/dashboard_controller.rb +13 -0
- data/app/controllers/journeybook/admin/media_assets_controller.rb +59 -0
- data/app/controllers/journeybook/admin/navigation_items_controller.rb +91 -0
- data/app/controllers/journeybook/admin/navigation_menus_controller.rb +59 -0
- data/app/controllers/journeybook/admin/pages_controller.rb +304 -0
- data/app/controllers/journeybook/admin/redirects_controller.rb +51 -0
- data/app/controllers/journeybook/admin/sites_controller.rb +51 -0
- data/app/controllers/journeybook/application_controller.rb +52 -0
- data/app/controllers/journeybook/form_submissions_controller.rb +55 -0
- data/app/helpers/journeybook/application_helper.rb +4 -0
- data/app/helpers/journeybook/navigation_helper.rb +11 -0
- data/app/javascript/journeybook/application.js +5 -0
- data/app/javascript/journeybook/component_search_controller.js +13 -0
- data/app/jobs/journeybook/application_job.rb +4 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/journeybook/ai_style_prompt.rb +6 -0
- data/app/models/journeybook/application_record.rb +5 -0
- data/app/models/journeybook/media_asset.rb +26 -0
- data/app/models/journeybook/navigation_item.rb +45 -0
- data/app/models/journeybook/navigation_menu.rb +23 -0
- data/app/models/journeybook/page.rb +96 -0
- data/app/models/journeybook/redirect.rb +31 -0
- data/app/models/journeybook/site.rb +62 -0
- data/app/services/journeybook/ai/markdown_generator.rb +15 -0
- data/app/services/journeybook/ai/openai_client.rb +54 -0
- data/app/services/journeybook/ai/prompt_builder.rb +154 -0
- data/app/services/journeybook/ai/template_validator.rb +17 -0
- data/app/services/journeybook/component_parser.rb +53 -0
- data/app/services/journeybook/component_renderer.rb +108 -0
- data/app/services/journeybook/component_tag_updater.rb +132 -0
- data/app/services/journeybook/component_validator.rb +111 -0
- data/app/services/journeybook/form_mailer.rb +69 -0
- data/app/services/journeybook/html_sanitizer.rb +46 -0
- data/app/services/journeybook/letter_opener_client.rb +14 -0
- data/app/services/journeybook/markdown_renderer.rb +29 -0
- data/app/services/journeybook/page_renderer.rb +58 -0
- data/app/services/journeybook/page_resolver.rb +55 -0
- data/app/services/journeybook/postmark_client.rb +48 -0
- data/app/services/journeybook/storage/s3_client.rb +96 -0
- data/app/views/journeybook/admin/ai_pages/_draft_form.html.erb +73 -0
- data/app/views/journeybook/admin/ai_pages/new.html.erb +43 -0
- data/app/views/journeybook/admin/ai_style_prompts/_form.html.erb +16 -0
- data/app/views/journeybook/admin/ai_style_prompts/edit.html.erb +2 -0
- data/app/views/journeybook/admin/ai_style_prompts/index.html.erb +26 -0
- data/app/views/journeybook/admin/ai_style_prompts/new.html.erb +2 -0
- data/app/views/journeybook/admin/components/_component.html.erb +17 -0
- data/app/views/journeybook/admin/components/_reference.html.erb +9 -0
- data/app/views/journeybook/admin/components/index.html.erb +46 -0
- data/app/views/journeybook/admin/components/preview.html.erb +13 -0
- data/app/views/journeybook/admin/dashboard/show.html.erb +34 -0
- data/app/views/journeybook/admin/media_assets/_reference.html.erb +16 -0
- data/app/views/journeybook/admin/media_assets/index.html.erb +43 -0
- data/app/views/journeybook/admin/media_assets/new.html.erb +41 -0
- data/app/views/journeybook/admin/navigation_items/_form.html.erb +46 -0
- data/app/views/journeybook/admin/navigation_items/edit.html.erb +2 -0
- data/app/views/journeybook/admin/navigation_items/new.html.erb +2 -0
- data/app/views/journeybook/admin/navigation_menus/_form.html.erb +10 -0
- data/app/views/journeybook/admin/navigation_menus/edit.html.erb +38 -0
- data/app/views/journeybook/admin/navigation_menus/index.html.erb +28 -0
- data/app/views/journeybook/admin/navigation_menus/new.html.erb +2 -0
- data/app/views/journeybook/admin/pages/_collection_field.html.erb +58 -0
- data/app/views/journeybook/admin/pages/_form.html.erb +77 -0
- data/app/views/journeybook/admin/pages/_image_picker_modal.html.erb +23 -0
- data/app/views/journeybook/admin/pages/_table.html.erb +37 -0
- data/app/views/journeybook/admin/pages/_url_field.html.erb +25 -0
- data/app/views/journeybook/admin/pages/edit.html.erb +547 -0
- data/app/views/journeybook/admin/pages/errors.html.erb +25 -0
- data/app/views/journeybook/admin/pages/index.html.erb +52 -0
- data/app/views/journeybook/admin/pages/new.html.erb +27 -0
- data/app/views/journeybook/admin/pages/new_component.html.erb +70 -0
- data/app/views/journeybook/admin/pages/public_preview.html.erb +27 -0
- data/app/views/journeybook/admin/pages/source.html.erb +23 -0
- data/app/views/journeybook/admin/redirects/_form.html.erb +9 -0
- data/app/views/journeybook/admin/redirects/edit.html.erb +2 -0
- data/app/views/journeybook/admin/redirects/index.html.erb +15 -0
- data/app/views/journeybook/admin/redirects/new.html.erb +2 -0
- data/app/views/journeybook/admin/shared/_drag_handle.html.erb +3 -0
- data/app/views/journeybook/admin/shared/_errors.html.erb +10 -0
- data/app/views/journeybook/admin/sites/_form.html.erb +12 -0
- data/app/views/journeybook/admin/sites/edit.html.erb +2 -0
- data/app/views/journeybook/admin/sites/index.html.erb +26 -0
- data/app/views/journeybook/admin/sites/new.html.erb +2 -0
- data/app/views/layouts/journeybook/application.html.erb +57 -0
- data/config/importmap.rb +3 -0
- data/config/routes.rb +39 -0
- data/db/migrate/20260627000001_create_journeybook_sites.rb +13 -0
- data/db/migrate/20260627000002_create_journeybook_pages.rb +23 -0
- data/db/migrate/20260627000003_create_journeybook_redirects.rb +13 -0
- data/db/migrate/20260628000004_add_context_fields_to_journeybook_sites.rb +38 -0
- data/db/migrate/20260628000005_create_journeybook_media_assets.rb +24 -0
- data/db/migrate/20260629000006_create_journeybook_navigation.rb +28 -0
- data/db/migrate/20260731000007_add_open_in_new_window_to_journeybook_navigation_items.rb +5 -0
- data/db/migrate/20260731000008_create_journeybook_ai_style_prompts.rb +12 -0
- data/db/migrate/20260801000009_add_blog_fields_to_journeybook_pages.rb +9 -0
- data/db/migrate/20260801000010_add_knowledge_base_fields_to_journeybook_pages.rb +8 -0
- data/db/migrate/20260801000011_add_position_to_journeybook_pages.rb +6 -0
- data/lib/generators/journeybook/install/install_generator.rb +72 -0
- data/lib/generators/journeybook/install/templates/add_blog_fields_to_journeybook_pages.rb +9 -0
- data/lib/generators/journeybook/install/templates/add_context_fields_to_journeybook_sites.rb +38 -0
- data/lib/generators/journeybook/install/templates/add_knowledge_base_fields_to_journeybook_pages.rb +8 -0
- data/lib/generators/journeybook/install/templates/add_open_in_new_window_to_journeybook_navigation_items.rb +5 -0
- data/lib/generators/journeybook/install/templates/add_position_to_journeybook_pages.rb +6 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_ai_style_prompts.rb +12 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_media_assets.rb +24 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_navigation.rb +28 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_pages.rb +33 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_redirects.rb +13 -0
- data/lib/generators/journeybook/install/templates/create_journeybook_sites.rb +13 -0
- data/lib/generators/journeybook/install/templates/journeybook.rb +42 -0
- data/lib/generators/journeybook/install/templates/pages_controller.rb +3 -0
- data/lib/generators/journeybook/install/templates/show.html.erb +10 -0
- data/lib/journeybook/component_registry.rb +136 -0
- data/lib/journeybook/configuration.rb +55 -0
- data/lib/journeybook/engine.rb +19 -0
- data/lib/journeybook/page_template_registry.rb +67 -0
- data/lib/journeybook/public_page_constraint.rb +23 -0
- data/lib/journeybook/version.rb +3 -0
- data/lib/journeybook.rb +62 -0
- data/lib/tasks/journeybook_tasks.rake +8 -0
- metadata +266 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f97196aa51bf528b293c97d43e0b4c9ef95821b0cce0e94b6db591892a62d4aa
|
|
4
|
+
data.tar.gz: dba603d13ec8b959fff8c21204c9a603e6d1a27264039ea031da5149b0d5ec7a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a44844e4f07e56eca00236cad81a17ccc31802285af382f683e551905820754d308b0f33305d982df293a72d6d6320fc614f281ce3e9631176af373e5868d535
|
|
7
|
+
data.tar.gz: f750446cff27d84651b81dc30b8353bae9dea4f93ed9b7e1af353fa089e80eb38d97f7c77ae099ed4716b980996ce52b65c6005d86c6d2965a3ef4b4afcb0cdc
|
data/CHANGELOG.md
ADDED
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2026 talent journey
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# Journeybook
|
|
2
|
+
|
|
3
|
+
Journeybook is a mountable Rails 8 engine for Markdown-first CMS content. It keeps layouts, routing, and design in the host application while providing reusable page models, component registration, and a rendering pipeline.
|
|
4
|
+
|
|
5
|
+
## Principles
|
|
6
|
+
|
|
7
|
+
- Content is written in Markdown.
|
|
8
|
+
- Components are defined in Ruby code, not stored in the database.
|
|
9
|
+
- Public routing belongs to the host application.
|
|
10
|
+
- No JavaScript dependency is required beyond Rails defaults such as Turbo.
|
|
11
|
+
- The engine exposes small, Zeitwerk-compatible Rails services.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Install the released gem from RubyGems:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "journeybook", "~> 0.1"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For local engine development only, a host application can temporarily use a relative `path:` dependency. Do not deploy a `path:` dependency.
|
|
28
|
+
|
|
29
|
+
## Initial host installation
|
|
30
|
+
|
|
31
|
+
Run these steps once for each Rails host application:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle install
|
|
35
|
+
bin/rails journeybook:install
|
|
36
|
+
bin/rails db:migrate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Commit the files generated by `journeybook:install`, including the copied migrations, initializer, controller, view, and routes. The installer copies Journeybook migrations and a reload-safe initializer, then adds a minimal public page controller, default page view, and host-owned routes:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
root "pages#show"
|
|
43
|
+
|
|
44
|
+
constraints Journeybook::PublicPageConstraint.new do
|
|
45
|
+
get "*path" => "pages#show", as: :journeybook_page
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The generated controller includes `Journeybook::PublicPages`, which resolves published pages by request host and path, renders them through `Journeybook::PageRenderer`, and leaves layout/design in the host app.
|
|
50
|
+
|
|
51
|
+
The admin routes are mounted by the installer:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
mount Journeybook::Engine => "/journeybook"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Open `/journeybook` in the host app to manage sites, pages, components, the image library, previews, and AI-generated Markdown drafts.
|
|
58
|
+
|
|
59
|
+
## Updating Journeybook
|
|
60
|
+
|
|
61
|
+
1. Update the version constraint in the host application's Gemfile when necessary.
|
|
62
|
+
2. Run `bundle update journeybook` and commit the resulting `Gemfile.lock`.
|
|
63
|
+
3. If the release includes database migrations, run `bin/rails journeybook:install` and commit the newly copied migrations.
|
|
64
|
+
4. Run `bin/rails db:migrate` locally and in the deployment release step.
|
|
65
|
+
5. Deploy the host application's commit.
|
|
66
|
+
|
|
67
|
+
Do not rerun the installer for a release without new migrations or generated setup changes. Do not edit migrations that have already been deployed; ship a new engine migration instead.
|
|
68
|
+
|
|
69
|
+
## Coolify deployment
|
|
70
|
+
|
|
71
|
+
For a Rails application deployed from `main` with Coolify, RubyGems requires no additional GitHub SSH key. Coolify runs `bundle install` as part of the normal build and resolves the version locked in `Gemfile.lock`.
|
|
72
|
+
|
|
73
|
+
Configure the production release command to run `bin/rails db:migrate`. Store Journeybook admin credentials, S3 credentials, and optional Postmark/OpenAI credentials as production secrets. Use persistent S3-compatible storage for the Journeybook image library; do not rely on the container filesystem for uploaded media.
|
|
74
|
+
|
|
75
|
+
## Releasing
|
|
76
|
+
|
|
77
|
+
RubyGems releases are immutable. Use semantic versioning and update both `lib/journeybook/version.rb` and `CHANGELOG.md` before releasing.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bin/rails db:test:prepare && bin/rails test
|
|
81
|
+
bin/rubocop -f github
|
|
82
|
+
bundle exec rake build
|
|
83
|
+
gem check pkg/journeybook-*.gem
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Run the Release workflow manually from GitHub Actions after the version bump and changelog commit are merged. It runs tests and linting, then `rubygems/release-gem` creates the matching Git tag and publishes the gem.
|
|
87
|
+
|
|
88
|
+
For RubyGems Trusted Publishing, configure the `journeybook` gem with this GitHub Actions publisher after the first release:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
Owner: talent-journey
|
|
92
|
+
Repository: journeybook
|
|
93
|
+
Workflow: .github/workflows/release.yml
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Trusted Publishing uses GitHub OIDC, so it needs no `RUBYGEMS_API_KEY` and no one-time MFA code. The first release must be published manually with an MFA code before a gem-specific publisher can be configured.
|
|
97
|
+
|
|
98
|
+
## Credentials
|
|
99
|
+
|
|
100
|
+
Journeybook expects secrets below one `journeybook` namespace in the host app credentials. Non-secret form settings such as sender and recipient addresses can stay in the initializer.
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
journeybook:
|
|
104
|
+
admin:
|
|
105
|
+
username: ...
|
|
106
|
+
password: ...
|
|
107
|
+
|
|
108
|
+
openai:
|
|
109
|
+
api_key: ...
|
|
110
|
+
model: gpt-5.4-mini
|
|
111
|
+
|
|
112
|
+
postmark:
|
|
113
|
+
api_key: ...
|
|
114
|
+
|
|
115
|
+
s3:
|
|
116
|
+
default:
|
|
117
|
+
region: eu-central-1
|
|
118
|
+
|
|
119
|
+
development:
|
|
120
|
+
bucket: journeybook-assets-development
|
|
121
|
+
access_key_id: ...
|
|
122
|
+
secret_access_key: ...
|
|
123
|
+
prefix: journeybook/development
|
|
124
|
+
|
|
125
|
+
staging:
|
|
126
|
+
bucket: journeybook-assets-staging
|
|
127
|
+
access_key_id: ...
|
|
128
|
+
secret_access_key: ...
|
|
129
|
+
prefix: journeybook/staging
|
|
130
|
+
|
|
131
|
+
production:
|
|
132
|
+
bucket: journeybook-assets-production
|
|
133
|
+
access_key_id: ...
|
|
134
|
+
secret_access_key: ...
|
|
135
|
+
prefix: journeybook/production
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Journeybook denies admin access by default. Configure `admin_authenticator` in the host application. For a small, single-admin installation, HTTP Basic authentication with the credentials above is sufficient; production applications with their own account system should delegate to that system instead.
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Journeybook.configure do |config|
|
|
142
|
+
config.admin_basic_auth username: journeybook_credentials.dig(:admin, :username), password: journeybook_credentials.dig(:admin, :password)
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Configure non-secret form delivery settings in the host initializer:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Journeybook.configure do |config|
|
|
150
|
+
config.form_from = "noreply@example.com"
|
|
151
|
+
config.form_to = "team@example.com"
|
|
152
|
+
config.form_delivery_method = :postmark
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The generated initializer reads `journeybook.s3.default` and merges it with the current `Rails.env` block. Use separate buckets per environment for production safety. If you intentionally use one bucket, restrict each environment's IAM credentials to its own prefix.
|
|
157
|
+
|
|
158
|
+
Journeybook builds public S3 URLs from `bucket` and `region` automatically. Set `public_base_url` only when images should be served through CloudFront, a CDN, or another custom public host:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
journeybook:
|
|
162
|
+
s3:
|
|
163
|
+
production:
|
|
164
|
+
public_base_url: https://cdn.example.com
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Image Library
|
|
168
|
+
|
|
169
|
+
Journeybook includes a global public image library for component props and AI-generated drafts. Uploaded images are stored in S3 and metadata is stored in Journeybook. Deleting an image from the library also deletes the S3 object. The admin UI accepts PNG, JPEG, WebP, GIF, and SVG files.
|
|
170
|
+
|
|
171
|
+
By default Journeybook stores objects below an environment-specific prefix such as `journeybook/development` or `journeybook/production`. Production should use a separate bucket or IAM credentials restricted to the production prefix, so local development cannot delete production objects.
|
|
172
|
+
|
|
173
|
+
## AI Page Builder
|
|
174
|
+
|
|
175
|
+
Journeybook can generate editable Markdown drafts with OpenAI. Store the API key under `journeybook.openai.api_key` in Rails credentials:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
journeybook:
|
|
179
|
+
openai:
|
|
180
|
+
api_key: ...
|
|
181
|
+
model: gpt-5.4-mini
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
You can also use `OPENAI_API_KEY` as a local development fallback:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
OPENAI_API_KEY=... bin/dev
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The AI builder sends the user's prompt plus the registered component schema to OpenAI and returns one Markdown document. It uses `gpt-5.4-mini` by default. Selecting a page template restricts generation to that template's component recipe and preserves its order. Journeybook validates the generated components before review; the generated Markdown is never published directly, and users must review and save it as a draft through the normal page form.
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
journeybook_credentials = Rails.application.credentials.fetch(:journeybook, {})
|
|
196
|
+
journeybook_s3_default = journeybook_credentials.dig(:s3, :default) || {}
|
|
197
|
+
journeybook_s3_env = journeybook_credentials.dig(:s3, Rails.env.to_sym) || {}
|
|
198
|
+
journeybook_s3 = journeybook_s3_default.merge(journeybook_s3_env)
|
|
199
|
+
|
|
200
|
+
Journeybook.configure do |config|
|
|
201
|
+
config.default_layout = "application"
|
|
202
|
+
config.openai_api_key = journeybook_credentials.dig(:openai, :api_key)
|
|
203
|
+
config.openai_model = journeybook_credentials.dig(:openai, :model) || config.openai_model
|
|
204
|
+
config.postmark_api_key = journeybook_credentials.dig(:postmark, :api_key)
|
|
205
|
+
config.form_from = "noreply@example.com"
|
|
206
|
+
config.form_to = "team@example.com"
|
|
207
|
+
config.form_delivery_method = Rails.env.development? ? :letter_opener : :postmark
|
|
208
|
+
config.s3_bucket = journeybook_s3[:bucket]
|
|
209
|
+
config.s3_region = journeybook_s3[:region]
|
|
210
|
+
config.s3_access_key_id = journeybook_s3[:access_key_id]
|
|
211
|
+
config.s3_secret_access_key = journeybook_s3[:secret_access_key]
|
|
212
|
+
config.s3_prefix = journeybook_s3[:prefix] || "journeybook/#{Rails.env}"
|
|
213
|
+
# Optional: use CloudFront, a CDN, or another custom public host instead of the default S3 URL.
|
|
214
|
+
config.s3_public_base_url = journeybook_s3[:public_base_url]
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Page templates
|
|
219
|
+
|
|
220
|
+
Page templates give non-technical editors an approved component recipe instead of a blank canvas. Register them in the host application:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
Journeybook.register_template :blog_post,
|
|
224
|
+
name: "Blog post",
|
|
225
|
+
description: "Article header and longform content.",
|
|
226
|
+
page_type: "blog_post",
|
|
227
|
+
path_prefix: "/blog",
|
|
228
|
+
path_hint: "/blog/your-post-title",
|
|
229
|
+
sites: %i[marketing],
|
|
230
|
+
markdown: <<~MARKDOWN
|
|
231
|
+
{{ article_hero }}
|
|
232
|
+
|
|
233
|
+
{{ text text="Start writing your article here." }}
|
|
234
|
+
MARKDOWN
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Editors choose a template when creating a page. The template supplies its component Markdown and page type. If `path_prefix` is present and the editor leaves the path empty, Journeybook derives a path from the title, such as `/blog/your-post-title`.
|
|
238
|
+
|
|
239
|
+
Use `page_type: "kb_article"` with an optional `parent_id` to build a Help Center hierarchy. Use `page_type: "glossary_term"` and `topic: "HR"` (or another topic) for glossary content and topic-specific index components.
|
|
240
|
+
|
|
241
|
+
## Components
|
|
242
|
+
|
|
243
|
+
Register components in the host application:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
Journeybook.configure do |config|
|
|
247
|
+
config.component :hero, Website::HeroComponent
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Then use them in Markdown:
|
|
252
|
+
|
|
253
|
+
```markdown
|
|
254
|
+
# Hello
|
|
255
|
+
|
|
256
|
+
{{ hero title="Journeybook" subtitle="Markdown-first CMS" }}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Registered component classes can expose either `.render(**props)` or instances with `#render`.
|
|
260
|
+
|
|
261
|
+
### Repeatable items
|
|
262
|
+
|
|
263
|
+
Use a `collection` prop for a component with a configurable number of homogeneous items. Define the item schema and an explicit visual limit:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
Journeybook.configure do |config|
|
|
267
|
+
config.component :feature_grid, Website::FeatureGridComponent,
|
|
268
|
+
props: {
|
|
269
|
+
items: {
|
|
270
|
+
type: :collection,
|
|
271
|
+
min: 1,
|
|
272
|
+
max: 4,
|
|
273
|
+
item: {
|
|
274
|
+
title: "Feature title",
|
|
275
|
+
description: { type: :long_text },
|
|
276
|
+
href: { type: :url, required: false },
|
|
277
|
+
color: { type: :select, options: %w[sunset forest stone] }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Collection data is JSON wrapped in single quotes, which keeps Markdown readable and gives the AI a deterministic format:
|
|
285
|
+
|
|
286
|
+
```markdown
|
|
287
|
+
{{ feature_grid items='[{"title":"Onboarding","description":"New hires start with confidence.","href":"/onboarding"}]' }}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The page editor renders collection props as a repeater with add, remove, and drag-to-reorder controls. Nested arbitrary components are intentionally not supported.
|
|
291
|
+
|
|
292
|
+
## Navigation
|
|
293
|
+
|
|
294
|
+
Journeybook stores site-scoped navigation menus and ordered items. An item links to either a published Journeybook page or an external URL/path. The host application keeps ownership of the markup and renders menus by key:
|
|
295
|
+
|
|
296
|
+
```erb
|
|
297
|
+
<% (journeybook_menu("footer_product")&.visible_items || []).each do |item| %>
|
|
298
|
+
<a href="<%= item.href %>"><%= item.label %></a>
|
|
299
|
+
<% end %>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Create and edit menus in the admin under **Navigation**. Seed menu keys such as `header_learn` or `footer_product` from the host application; their meaning is defined by that host's layout.
|
|
303
|
+
|
|
304
|
+
## Form Emails
|
|
305
|
+
|
|
306
|
+
Journeybook includes a public form endpoint for components that need to send notification emails through Postmark. Store the Postmark API key under `journeybook.postmark.api_key`, then configure the non-secret form addresses in the host app initializer:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
Journeybook.configure do |config|
|
|
310
|
+
config.postmark_api_key = Rails.application.credentials.dig(:journeybook, :postmark, :api_key)
|
|
311
|
+
config.form_from = "noreply@example.com"
|
|
312
|
+
config.form_to = "team@example.com"
|
|
313
|
+
config.form_delivery_method = :postmark
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For local development, add `letter_opener` to the host app's `development` group and switch the delivery method there. Journeybook does not include `letter_opener` as a runtime dependency because it should not be installed in production apps unless the host app explicitly wants it.
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
# Gemfile
|
|
321
|
+
group :development do
|
|
322
|
+
gem "letter_opener"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# config/initializers/journeybook.rb
|
|
326
|
+
Journeybook.configure do |config|
|
|
327
|
+
config.form_delivery_method = Rails.env.development? ? :letter_opener : :postmark
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Components can render forms that post to the configured action:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
class ContactFormComponent
|
|
335
|
+
def self.render
|
|
336
|
+
<<~HTML
|
|
337
|
+
<form action="#{Journeybook.configuration.form_action}" method="post">
|
|
338
|
+
<input name="name" required>
|
|
339
|
+
<input name="email" type="email" required>
|
|
340
|
+
<textarea name="message" required></textarea>
|
|
341
|
+
<button type="submit">Send</button>
|
|
342
|
+
</form>
|
|
343
|
+
HTML
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The recipient address is configuration-only. Submitted `email` is used as the Postmark `ReplyTo` address when present. If you mount Journeybook somewhere other than `/journeybook`, set `config.form_action` to the matching form endpoint path.
|
|
349
|
+
|
|
350
|
+
## Contributing
|
|
351
|
+
Bug reports and pull requests are welcome at https://github.com/talent-journey/journeybook.
|
|
352
|
+
|
|
353
|
+
## License
|
|
354
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
File without changes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Journeybook</title>
|
|
3
|
+
<rect width="160" height="160" rx="36" fill="#0F172A"/>
|
|
4
|
+
<path d="M52 30H118V95C118 118.196 99.196 137 76 137H69C47.461 137 30 119.539 30 98V91H59V98C59 103.523 63.477 108 69 108H76C83.18 108 89 102.18 89 95V59H52V30Z" fill="#F8FAFC"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const initSortableLists = () => {
|
|
3
|
+
document.querySelectorAll("[data-sortable-list]").forEach((list) => {
|
|
4
|
+
if (list.dataset.sortableReady === "true") return
|
|
5
|
+
list.dataset.sortableReady = "true"
|
|
6
|
+
|
|
7
|
+
let draggedItem = null
|
|
8
|
+
let changed = false
|
|
9
|
+
|
|
10
|
+
list.addEventListener("dragstart", (event) => {
|
|
11
|
+
const item = event.target.closest("[data-sortable-item]")
|
|
12
|
+
if (!item || !list.contains(item)) return
|
|
13
|
+
|
|
14
|
+
draggedItem = item
|
|
15
|
+
changed = false
|
|
16
|
+
item.classList.add("is-dragging")
|
|
17
|
+
event.dataTransfer.effectAllowed = "move"
|
|
18
|
+
event.dataTransfer.setData("text/plain", item.dataset.sortableId || "")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
list.addEventListener("dragover", (event) => {
|
|
22
|
+
if (!draggedItem) return
|
|
23
|
+
|
|
24
|
+
event.preventDefault()
|
|
25
|
+
event.dataTransfer.dropEffect = "move"
|
|
26
|
+
|
|
27
|
+
const items = [ ...list.querySelectorAll(":scope > [data-sortable-item]") ]
|
|
28
|
+
const target = items.find((item) => {
|
|
29
|
+
if (item === draggedItem) return false
|
|
30
|
+
const rect = item.getBoundingClientRect()
|
|
31
|
+
return event.clientY < rect.top + rect.height / 2
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (target) {
|
|
35
|
+
list.insertBefore(draggedItem, target)
|
|
36
|
+
} else {
|
|
37
|
+
list.appendChild(draggedItem)
|
|
38
|
+
}
|
|
39
|
+
changed = true
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
list.addEventListener("drop", (event) => event.preventDefault())
|
|
43
|
+
|
|
44
|
+
list.addEventListener("dragend", async () => {
|
|
45
|
+
if (!draggedItem) return
|
|
46
|
+
|
|
47
|
+
draggedItem.classList.remove("is-dragging")
|
|
48
|
+
const shouldSave = changed
|
|
49
|
+
draggedItem = null
|
|
50
|
+
changed = false
|
|
51
|
+
if (!shouldSave) return
|
|
52
|
+
|
|
53
|
+
const params = new URLSearchParams()
|
|
54
|
+
list.querySelectorAll(":scope > [data-sortable-item]").forEach((item) => {
|
|
55
|
+
params.append(`${list.dataset.sortableParam}[]`, item.dataset.sortableId)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await fetch(list.dataset.sortableUrl, {
|
|
60
|
+
method: "PATCH",
|
|
61
|
+
credentials: "same-origin",
|
|
62
|
+
headers: {
|
|
63
|
+
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")?.content || "",
|
|
64
|
+
"Accept": "text/html",
|
|
65
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
66
|
+
},
|
|
67
|
+
body: params
|
|
68
|
+
})
|
|
69
|
+
} finally {
|
|
70
|
+
window.location.reload()
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const initFlashes = () => {
|
|
77
|
+
document.querySelectorAll("[data-flash]").forEach((flash) => {
|
|
78
|
+
if (flash.dataset.dismissReady === "true") return
|
|
79
|
+
flash.dataset.dismissReady = "true"
|
|
80
|
+
window.setTimeout(() => flash.remove(), 2000)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const init = () => {
|
|
85
|
+
initSortableLists()
|
|
86
|
+
initFlashes()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (document.readyState === "loading") {
|
|
90
|
+
document.addEventListener("DOMContentLoaded", init)
|
|
91
|
+
} else {
|
|
92
|
+
init()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
document.addEventListener("turbo:load", init)
|
|
96
|
+
})()
|