bunko 0.2.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 +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +351 -0
- data/LICENSE.txt +21 -0
- data/README.md +641 -0
- data/ROADMAP.md +519 -0
- data/Rakefile +10 -0
- data/lib/bunko/configuration.rb +180 -0
- data/lib/bunko/controllers/acts_as.rb +22 -0
- data/lib/bunko/controllers/collection.rb +160 -0
- data/lib/bunko/controllers.rb +5 -0
- data/lib/bunko/models/acts_as.rb +24 -0
- data/lib/bunko/models/post_methods/publishable.rb +51 -0
- data/lib/bunko/models/post_methods/sluggable.rb +47 -0
- data/lib/bunko/models/post_methods/word_countable.rb +76 -0
- data/lib/bunko/models/post_methods.rb +75 -0
- data/lib/bunko/models/post_type_methods.rb +18 -0
- data/lib/bunko/models.rb +6 -0
- data/lib/bunko/railtie.rb +22 -0
- data/lib/bunko/routing/mapper_methods.rb +103 -0
- data/lib/bunko/routing.rb +4 -0
- data/lib/bunko/version.rb +5 -0
- data/lib/bunko.rb +11 -0
- data/lib/tasks/bunko/add.rake +259 -0
- data/lib/tasks/bunko/helpers.rb +25 -0
- data/lib/tasks/bunko/install.rake +125 -0
- data/lib/tasks/bunko/sample_data.rake +128 -0
- data/lib/tasks/bunko/setup.rake +186 -0
- data/lib/tasks/support/sample_data_generator.rb +399 -0
- data/lib/tasks/templates/INSTALL.md +62 -0
- data/lib/tasks/templates/config/initializers/bunko.rb.tt +45 -0
- data/lib/tasks/templates/controllers/controller.rb.tt +25 -0
- data/lib/tasks/templates/controllers/pages_controller.rb.tt +29 -0
- data/lib/tasks/templates/db/migrate/create_post_types.rb.tt +14 -0
- data/lib/tasks/templates/db/migrate/create_posts.rb.tt +31 -0
- data/lib/tasks/templates/models/post.rb.tt +8 -0
- data/lib/tasks/templates/models/post_type.rb.tt +8 -0
- data/lib/tasks/templates/views/collections/index.html.erb.tt +67 -0
- data/lib/tasks/templates/views/collections/show.html.erb.tt +39 -0
- data/lib/tasks/templates/views/layouts/bunko_footer.html.erb.tt +3 -0
- data/lib/tasks/templates/views/layouts/bunko_nav.html.erb.tt +9 -0
- data/lib/tasks/templates/views/layouts/bunko_styles.html.erb.tt +3 -0
- data/lib/tasks/templates/views/pages/show.html.erb.tt +16 -0
- data/sig/bunko.rbs +4 -0
- metadata +116 -0
data/ROADMAP.md
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# Bunko 1.0 Roadmap
|
|
2
|
+
|
|
3
|
+
**Goal:** Ship a production-ready CMS gem where a Rails developer can add `gem "bunko"`, run a couple generators, and have a working blog in under 5 minutes. Officially we are only targeting support for Rails but we are trying to keep dependencies as light as possible.
|
|
4
|
+
|
|
5
|
+
**Note:** Version 0.1.0 was released as a placeholder to register the gem name. We're now building toward 1.0.0, using 0.x versions during active development.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Development Status
|
|
10
|
+
|
|
11
|
+
- [x] **Milestone 1: Post Model Behavior** - ✅ COMPLETED
|
|
12
|
+
- [x] **Milestone 2: Collection Controllers** - ✅ COMPLETED
|
|
13
|
+
- [x] **Milestone 3: Installation Generator** - ✅ COMPLETED
|
|
14
|
+
- [x] **Milestone 4: Routing Helpers** - ✅ COMPLETED
|
|
15
|
+
- [x] **Milestone 5: Post Convenience Methods** - ✅ COMPLETED
|
|
16
|
+
- [x] **Milestone 6: Static Pages** - ✅ COMPLETED
|
|
17
|
+
- [ ] **Milestone 7: Configuration** - 🚧 PENDING (core system exists, may need expansion)
|
|
18
|
+
- [ ] **Milestone 8: Documentation** - 🚧 PENDING
|
|
19
|
+
- [ ] **Milestone 9: Release** - 🚧 PENDING
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Success Criteria
|
|
24
|
+
|
|
25
|
+
By 1.0, a Rails developer should be able to:
|
|
26
|
+
|
|
27
|
+
1. ✅ Install Bunko and generate a working blog in < 5 minutes
|
|
28
|
+
2. ✅ Add a second content collection (e.g., `/docs`) in < 2 minutes
|
|
29
|
+
3. ✅ Attach the text editor of their choice to create and edit posts
|
|
30
|
+
4. ✅ Schedule posts for future publication
|
|
31
|
+
5. ✅ Organize content into different post types without migrations
|
|
32
|
+
6. ✅ Customize views with their own HTML/CSS
|
|
33
|
+
7. ✅ Automatically use slug-based URLs instead of IDs
|
|
34
|
+
8. ✅ Create standalone pages (About, Contact, etc.) without full collections
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Milestone 1: Post Model Behavior
|
|
39
|
+
|
|
40
|
+
**Spec:** A Post model with Bunko enabled should have all essential CMS functionality.
|
|
41
|
+
|
|
42
|
+
### Required Behavior
|
|
43
|
+
|
|
44
|
+
**Scopes & Queries:**
|
|
45
|
+
- Developer can query `Post.published` and only see published posts with `published_at <= Time.current`
|
|
46
|
+
- Developer can query `Post.draft` and only see draft posts
|
|
47
|
+
- Developer can query `Post.scheduled` and only see posts scheduled for future publication
|
|
48
|
+
- Developer can filter posts by type: `Post.by_post_type('blog')` or similar API
|
|
49
|
+
- Default ordering shows most recent posts first
|
|
50
|
+
|
|
51
|
+
**Slug Generation:**
|
|
52
|
+
- When a post is created without a slug, one is auto-generated from the title
|
|
53
|
+
- Slugs are URL-safe (e.g., "Hello World" becomes "hello-world")
|
|
54
|
+
- Slugs are unique within their post_type
|
|
55
|
+
- Developer can provide custom slug and it won't be overwritten
|
|
56
|
+
|
|
57
|
+
**Publishing Workflow:**
|
|
58
|
+
- Post status can be: 'draft', 'published', or 'scheduled'
|
|
59
|
+
- When status changes to 'published' and `published_at` is blank, it auto-sets to current time
|
|
60
|
+
- Posts with status='published' but `published_at` in future are treated as scheduled
|
|
61
|
+
- Invalid status values are rejected
|
|
62
|
+
|
|
63
|
+
**Reading Metrics:**
|
|
64
|
+
- If post has `word_count`, developer can get estimated reading time
|
|
65
|
+
- Reading time calculation is configurable (default ~250 words/minute)
|
|
66
|
+
|
|
67
|
+
**Routing Support:**
|
|
68
|
+
- Users should be able to route #index/#show collections of posts with a simple routes entry, eg something like "mount_bunko :case_studies" should automatically show all posts where post_type = 'case_study' (or 'case_studies'?) in the subfolder /case-studies/ with the slug for that post. Similar to if they wrote this:
|
|
69
|
+
- resources :case_studies, controller: 'case_studies', path: 'case-studies', param: :slug, only: %i[index show]
|
|
70
|
+
- Users should possibly be able to route their core Posts model behind their existing admin / auth area, similar to how they mount sidekiq. This section is intended to be admin only for post editing - so full CRUD but not publicly visible. This should be optional, eg if they want to use a tool like Avo with their [Rhino editor](https://docs.avohq.io/3.0/fields/rhino.html) or [Markdown editor](https://docs.avohq.io/3.0/fields/markdown.html), they don't need to mount this section at all. https://avohq.io/
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
require "bunko/editor" # require the web UI
|
|
74
|
+
|
|
75
|
+
Rails.application.routes.draw do
|
|
76
|
+
mount Bunko::Editor => "/posts" # access it at http://localhost:3000/posts
|
|
77
|
+
...
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Acceptance Test
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# Developer can do this:
|
|
85
|
+
class Post < ApplicationRecord
|
|
86
|
+
acts_as_bunko_post
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# And get this behavior:
|
|
90
|
+
post = Post.create!(title: "Hello World", content: "...", post_type: 'blog')
|
|
91
|
+
post.slug # => "hello-world"
|
|
92
|
+
post.to_param # => "hello-world"
|
|
93
|
+
post.status # => "draft"
|
|
94
|
+
|
|
95
|
+
post.update!(status: 'published')
|
|
96
|
+
post.published_at # => Time.current (auto-set)
|
|
97
|
+
|
|
98
|
+
Post.published.count # => 1
|
|
99
|
+
Post.draft.count # => 0
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Milestone 2: Collection Controllers
|
|
105
|
+
|
|
106
|
+
**Spec:** A controller should be able to serve a content collection with minimal code.
|
|
107
|
+
|
|
108
|
+
### Required Behavior
|
|
109
|
+
|
|
110
|
+
**Defaults**
|
|
111
|
+
- If user just adds a routes, all of our standard behaviors should be observed.
|
|
112
|
+
- If user chooses to generate their own controller, we should allow that.
|
|
113
|
+
- If user wants to use our controllers but adjust settings through bunko.rb initializer, we should allow that.
|
|
114
|
+
|
|
115
|
+
**Index Action:**
|
|
116
|
+
- Shows all published posts for a given post_type
|
|
117
|
+
- Does not leak other configured posts types in any way or scopes
|
|
118
|
+
- Paginates results - recommend pagy but this should be adaptable? Or perhaps enable Pagy or kamanari behavior in a bunko.rb initializer
|
|
119
|
+
- Allows customization of per_page, ordering, layout
|
|
120
|
+
- Provides access to collection name in views
|
|
121
|
+
|
|
122
|
+
**Show Action:**
|
|
123
|
+
- Finds post by slug
|
|
124
|
+
- Scoped to the correct - eg you should be able to have same slug on 2 different post types
|
|
125
|
+
- Returns 404 if not found (and routes doesn't take over with a 301 or something)
|
|
126
|
+
- Provides access to the post in views
|
|
127
|
+
|
|
128
|
+
**Multiple Post Types:**
|
|
129
|
+
- Single controller can serve multiple related post types
|
|
130
|
+
- Example: Resources controller serves guides, templates, checklists
|
|
131
|
+
|
|
132
|
+
### Acceptance Test
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
# Developer can do this:
|
|
136
|
+
class BlogController < ApplicationController
|
|
137
|
+
bunko_collection :blog # or whatever the API is
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# And get these routes working:
|
|
141
|
+
GET /blog # => BlogController#index (lists blog posts)
|
|
142
|
+
GET /blog/:slug # => BlogController#show (shows single post)
|
|
143
|
+
|
|
144
|
+
# Views have access to:
|
|
145
|
+
@posts # in index
|
|
146
|
+
@post # in show
|
|
147
|
+
@collection_name # 'blog'
|
|
148
|
+
|
|
149
|
+
# Developer can customize:
|
|
150
|
+
class ChangelogController < ApplicationController
|
|
151
|
+
bunko_collection :changelog, per_page: 20, layout: 'docs'
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Milestone 3: Installation Generator
|
|
158
|
+
|
|
159
|
+
**Spec:** Running `rails bunko:install` should create everything needed for a working blog.
|
|
160
|
+
|
|
161
|
+
**Implementation Note:** This milestone was implemented as a two-phase pattern:
|
|
162
|
+
1. `rails bunko:install` - Creates migrations, models, and initializer
|
|
163
|
+
2. `rails bunko:setup` - Generates controllers, views, and routes based on configuration
|
|
164
|
+
|
|
165
|
+
This approach allows users to customize their post types in the initializer before generating the controllers/views, and makes it easy to add new collections later.
|
|
166
|
+
|
|
167
|
+
### Required Behavior
|
|
168
|
+
|
|
169
|
+
**Migration Creation:**
|
|
170
|
+
- Detects database type (PostgreSQL, SQLite, MySQL) and generates appropriate migration
|
|
171
|
+
- Creates `posts` table with essential fields:
|
|
172
|
+
- `title` (string, required)
|
|
173
|
+
- `slug` (string, required, indexed)
|
|
174
|
+
- `content` (text by default, json/jsonb with `--json-content` flag for JSON-based editors)
|
|
175
|
+
- `post_type` (references, required)
|
|
176
|
+
- `status` (string, indexed, default: 'draft') # should this be references to a post_status table?
|
|
177
|
+
- `published_at` (datetime, indexed)
|
|
178
|
+
- Timestamps for created_at and updated_at
|
|
179
|
+
- Creates `post_types` table with essential fields:
|
|
180
|
+
- `name` (string, required)
|
|
181
|
+
- `slug` (string, required, indexed)
|
|
182
|
+
- Timestamps
|
|
183
|
+
- Adds unique constraint on `[slug, post_type]`
|
|
184
|
+
- A post should always have only one post_type, or allow nil? Never multiple though. By default we should load in Post? Or allow nil?
|
|
185
|
+
- Optional fields based on flags (see Generator Options below)
|
|
186
|
+
- User should be able to override our migration and add something like "acts_as_post" to the model to get the same behaviors.
|
|
187
|
+
|
|
188
|
+
**Model Generation:**
|
|
189
|
+
- Creates `app/models/post.rb` with Bunko enabled
|
|
190
|
+
- Creates `app/models/post_type.rb` with Bunko enabled
|
|
191
|
+
- Includes comments explaining customization
|
|
192
|
+
|
|
193
|
+
**Controller Generation:**
|
|
194
|
+
- Creates `app/controllers/blog_controller.rb`
|
|
195
|
+
- Configured to serve 'blog' post type
|
|
196
|
+
- Includes comments explaining how to add more collections
|
|
197
|
+
|
|
198
|
+
**View Generation:**
|
|
199
|
+
- Creates `app/views/blog/index.html.erb` with semantic HTML
|
|
200
|
+
- Creates `app/views/blog/show.html.erb` with semantic HTML
|
|
201
|
+
- No CSS, no JavaScript - just clean HTML with helpful comments
|
|
202
|
+
- Shows title, content, published date, reading time
|
|
203
|
+
|
|
204
|
+
**Route Generation:**
|
|
205
|
+
- Adds routes for blog (index + show)
|
|
206
|
+
- Uses slug as param, not ID
|
|
207
|
+
|
|
208
|
+
**Initializer Generation:**
|
|
209
|
+
- Creates `config/initializers/bunko.rb`
|
|
210
|
+
- Includes common configuration options (commented out with examples)
|
|
211
|
+
|
|
212
|
+
**Post-Install Message:**
|
|
213
|
+
- Shows next steps (run migration, create first post)
|
|
214
|
+
- Links to documentation
|
|
215
|
+
|
|
216
|
+
### Generator Options
|
|
217
|
+
|
|
218
|
+
- `--skip-seo` - Skip adding SEO fields (title_tag, meta_description)
|
|
219
|
+
- `--json-content` - Use json/jsonb for content field instead of text (for JSON-based editors)
|
|
220
|
+
|
|
221
|
+
### Acceptance Test
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Developer runs:
|
|
225
|
+
$ rails bunko:install
|
|
226
|
+
$ rails db:migrate
|
|
227
|
+
|
|
228
|
+
# Result: They can visit /blog and see a working (empty) blog
|
|
229
|
+
# They can create a post in console and see it at /blog/post-slug
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Milestone 4: Routing Helpers
|
|
235
|
+
|
|
236
|
+
**Spec:** Setting up routes for collections should be simple and conventional.
|
|
237
|
+
|
|
238
|
+
### Required Behavior
|
|
239
|
+
|
|
240
|
+
**Route DSL:**
|
|
241
|
+
- Developer can call `bunko_collection :blog` instead of writing full resources line
|
|
242
|
+
- Supports custom paths (e.g., `bunko_collection :case_study, path: 'case-studies'`)
|
|
243
|
+
- Supports limiting actions (e.g., `only: [:index]` for index-only collection) # not critical
|
|
244
|
+
- Supports custom controller names
|
|
245
|
+
|
|
246
|
+
### Acceptance Test
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
# Developer can do this in config/routes.rb:
|
|
250
|
+
Rails.application.routes.draw do
|
|
251
|
+
bunko_collection :blog
|
|
252
|
+
bunko_collection :docs
|
|
253
|
+
bunko_collection :case_study, path: 'case-studies' #perhaps path should be automatically hyphenated and/or inflected/pluralized?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# And get these routes:
|
|
257
|
+
# /blog => blog#index
|
|
258
|
+
# /blog/:slug => blog#show
|
|
259
|
+
# /docs => docs#index
|
|
260
|
+
# /docs/:slug => docs#show
|
|
261
|
+
# /case-studies => case_study#index
|
|
262
|
+
# /case-studies/:slug => case_study#show
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Milestone 5: Post Convenience Methods
|
|
268
|
+
|
|
269
|
+
**Spec:** Common CMS view patterns should be available as Post instance methods for clean, conflict-free usage in views.
|
|
270
|
+
|
|
271
|
+
**Implementation Note:** Originally planned as view helpers, we decided to implement these as Post model methods instead. This approach:
|
|
272
|
+
- Avoids namespace conflicts (no `bunko_` prefix needed for generic helper names)
|
|
273
|
+
- Keeps views cleaner (`post.excerpt` vs `bunko_excerpt(post)`)
|
|
274
|
+
- Works identically in index loops and show views
|
|
275
|
+
|
|
276
|
+
### Required Behavior
|
|
277
|
+
|
|
278
|
+
**Content Formatting:**
|
|
279
|
+
- `post.excerpt(length: 160, omission: "...")` - returns truncated content, strips HTML, preserves word boundaries
|
|
280
|
+
- `post.reading_time_text` - returns "X min read" string (extends existing `reading_time` integer method)
|
|
281
|
+
|
|
282
|
+
**Date Formatting:**
|
|
283
|
+
- `post.published_date(format = :long)` - returns formatted published_at using I18n.l
|
|
284
|
+
- Supports Rails date formats: `:long`, `:short`, `:db`, custom strftime
|
|
285
|
+
|
|
286
|
+
**Navigation:**
|
|
287
|
+
- Not needed - routing DSL automatically generates helpers like `blog_path`, `blog_post_path(post)`
|
|
288
|
+
|
|
289
|
+
### Acceptance Test
|
|
290
|
+
|
|
291
|
+
```erb
|
|
292
|
+
<!-- Index view: loop over posts -->
|
|
293
|
+
<% @posts.each do |post| %>
|
|
294
|
+
<article>
|
|
295
|
+
<h2><%= link_to post.title, blog_post_path(post) %></h2>
|
|
296
|
+
<p class="meta">
|
|
297
|
+
<%= post.published_date %> · <%= post.reading_time_text %>
|
|
298
|
+
</p>
|
|
299
|
+
<p><%= post.excerpt %></p>
|
|
300
|
+
</article>
|
|
301
|
+
<% end %>
|
|
302
|
+
|
|
303
|
+
<!-- Show view: single post -->
|
|
304
|
+
<article>
|
|
305
|
+
<h1><%= @post.title %></h1>
|
|
306
|
+
<p class="meta">
|
|
307
|
+
<%= @post.published_date(:long) %> · <%= @post.reading_time_text %>
|
|
308
|
+
</p>
|
|
309
|
+
<div class="content">
|
|
310
|
+
<%= @post.content %>
|
|
311
|
+
</div>
|
|
312
|
+
</article>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Milestone 6: Static Pages
|
|
318
|
+
|
|
319
|
+
**Spec:** Developers should be able to create standalone pages (About, Contact, Privacy Policy) without needing a full collection with an index page.
|
|
320
|
+
|
|
321
|
+
### Required Behavior
|
|
322
|
+
|
|
323
|
+
**Routing DSL:**
|
|
324
|
+
- `bunko_page :about` creates a single route: `GET /about → pages#show`
|
|
325
|
+
- Supports custom paths: `bunko_page :about, path: "about-us"`
|
|
326
|
+
- Supports custom controllers: `bunko_page :contact, controller: "static_pages"`
|
|
327
|
+
- Works with namespaces: `namespace :legal do bunko_page :privacy end`
|
|
328
|
+
|
|
329
|
+
**PagesController:**
|
|
330
|
+
- Single shared controller for all pages (no per-page controller generation)
|
|
331
|
+
- Smart view resolution: checks for custom page template (e.g., `pages/about.html.erb`) first
|
|
332
|
+
- Falls back to default `pages/show.html.erb` if custom template doesn't exist
|
|
333
|
+
- Raises 404 if page Post not found
|
|
334
|
+
|
|
335
|
+
**Configuration:**
|
|
336
|
+
- Opt-out support: `config.allow_static_pages = false`
|
|
337
|
+
- Reserved "pages" post_type namespace with validation error
|
|
338
|
+
- Auto-generated during `rails bunko:setup` if enabled (default: true)
|
|
339
|
+
|
|
340
|
+
**Architecture:**
|
|
341
|
+
- Uses same Post model as collections (maintains one-model architecture)
|
|
342
|
+
- Pages stored with `post_type = "pages"`
|
|
343
|
+
- Slug must match route name (e.g., route `bunko_page :about` expects Post with slug "about")
|
|
344
|
+
|
|
345
|
+
### Acceptance Test
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
# In config/routes.rb:
|
|
349
|
+
Rails.application.routes.draw do
|
|
350
|
+
bunko_page :about
|
|
351
|
+
bunko_page :contact
|
|
352
|
+
bunko_page :privacy
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Create page content:
|
|
356
|
+
pages_type = PostType.find_by(name: "pages")
|
|
357
|
+
|
|
358
|
+
Post.create!(
|
|
359
|
+
title: "About Us",
|
|
360
|
+
content: "<p>Welcome to our company...</p>",
|
|
361
|
+
post_type: pages_type,
|
|
362
|
+
slug: "about", # Must match route name
|
|
363
|
+
status: "published"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Result:
|
|
367
|
+
# GET /about → renders pages/about.html.erb or pages/show.html.erb
|
|
368
|
+
# GET /contact → 404 (no Post with slug "contact" exists yet)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Milestone 7: Configuration
|
|
374
|
+
|
|
375
|
+
**Spec:** Bunko behavior should be customizable via initializer without modifying gem code.
|
|
376
|
+
|
|
377
|
+
### Required Behavior
|
|
378
|
+
|
|
379
|
+
**Configurable Options:**
|
|
380
|
+
- Post model name (default: 'Post')
|
|
381
|
+
- Valid post types (default: ['post'])
|
|
382
|
+
- Valid statuses (default: ['draft', 'published', 'scheduled'])
|
|
383
|
+
- Default status (default: 'draft')
|
|
384
|
+
- Reading speed in words/minute (default: 250)
|
|
385
|
+
- Excerpt length (default: 160)
|
|
386
|
+
- Slug generation strategy (default: parameterize)
|
|
387
|
+
|
|
388
|
+
**Configuration API:**
|
|
389
|
+
- Developer uses block syntax in initializer
|
|
390
|
+
- Configuration is globally accessible
|
|
391
|
+
- Invalid configuration values are validated
|
|
392
|
+
|
|
393
|
+
### Acceptance Test
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
# Developer can configure in config/initializers/bunko.rb:
|
|
397
|
+
Bunko.configure do |config|
|
|
398
|
+
config.post_type "post"
|
|
399
|
+
config.post_type "page"
|
|
400
|
+
config.post_type "doc"
|
|
401
|
+
config.post_type "tutorial"
|
|
402
|
+
|
|
403
|
+
config.reading_speed = 200
|
|
404
|
+
config.excerpt_length = 200
|
|
405
|
+
config.slug_generator = ->(title) { title.parameterize.truncate(50) }
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# And it affects behavior:
|
|
409
|
+
post = Post.create!(title: "Very Long Title...")
|
|
410
|
+
# slug uses custom generator
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Milestone 8: Documentation
|
|
416
|
+
|
|
417
|
+
**Spec:** Documentation should be excellent, examples should be practical.
|
|
418
|
+
|
|
419
|
+
### Required Deliverables
|
|
420
|
+
|
|
421
|
+
**README.md:**
|
|
422
|
+
- Philosophy and goals clearly stated
|
|
423
|
+
- Installation instructions (add to Gemfile, run generator)
|
|
424
|
+
- Quick start guide (5 minute blog)
|
|
425
|
+
- Multi-collection setup example
|
|
426
|
+
- Configuration options documented
|
|
427
|
+
- Customization patterns explained
|
|
428
|
+
- What Bunko doesn't do (auth, admin UI, etc.)
|
|
429
|
+
|
|
430
|
+
**EXAMPLES.md:**
|
|
431
|
+
- Basic blog setup
|
|
432
|
+
- Blog + docs setup
|
|
433
|
+
- Custom fields using metadata
|
|
434
|
+
- Custom scopes
|
|
435
|
+
- Overriding views
|
|
436
|
+
- Integration with admin gems (Avo, Administrate, etc.)
|
|
437
|
+
|
|
438
|
+
**Code Documentation:**
|
|
439
|
+
- All public APIs have clear documentation
|
|
440
|
+
- Generated code includes helpful comments
|
|
441
|
+
- Configuration options explained in generated initializer
|
|
442
|
+
|
|
443
|
+
**Example Apps:**
|
|
444
|
+
- `examples/basic_blog` - Minimal blog
|
|
445
|
+
- `examples/multi_collection` - Blog + docs + changelog
|
|
446
|
+
|
|
447
|
+
### Acceptance Test
|
|
448
|
+
|
|
449
|
+
New developer can:
|
|
450
|
+
- Read README and understand what Bunko does in < 2 minutes
|
|
451
|
+
- Follow quick start and have working blog in < 5 minutes
|
|
452
|
+
- Find answer to "how do I customize X?" in documentation
|
|
453
|
+
- Clone an example app and see Bunko in action
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Milestone 9: Release
|
|
458
|
+
|
|
459
|
+
**Spec:** 1.0.0 is published to RubyGems and ready for production use.
|
|
460
|
+
|
|
461
|
+
### Required Before Release
|
|
462
|
+
|
|
463
|
+
**Compatibility:**
|
|
464
|
+
- Works with Rails 8.0+ and follows Rails EOL maintenance policy
|
|
465
|
+
- Works with Ruby 3.2, 3.3, 3.4 and follows Ruby EOL maintenance policy
|
|
466
|
+
- Works with PostgreSQL, SQLite, MySQL
|
|
467
|
+
- Test coverage > 90%
|
|
468
|
+
- All Standard linter checks pass
|
|
469
|
+
|
|
470
|
+
**Package:**
|
|
471
|
+
- bunko.gemspec has no TODOs
|
|
472
|
+
- Proper description and summary
|
|
473
|
+
- Correct homepage and source URLs
|
|
474
|
+
- Appropriate version number (1.0.0)
|
|
475
|
+
- CHANGELOG.md updated
|
|
476
|
+
|
|
477
|
+
**Documentation:**
|
|
478
|
+
- README complete and accurate
|
|
479
|
+
- EXAMPLES.md or docs have practical examples
|
|
480
|
+
- Generated code has helpful comments
|
|
481
|
+
- GitHub release notes written
|
|
482
|
+
|
|
483
|
+
**Distribution:**
|
|
484
|
+
- Gem builds successfully
|
|
485
|
+
- Gem published to RubyGems
|
|
486
|
+
- GitHub release created
|
|
487
|
+
- Installation tested in fresh Rails app
|
|
488
|
+
|
|
489
|
+
### Acceptance Test
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
# Any developer can:
|
|
493
|
+
$ gem install bunko
|
|
494
|
+
$ rails new myblog
|
|
495
|
+
$ cd myblog
|
|
496
|
+
$ bundle add bunko
|
|
497
|
+
$ rails bunko:install
|
|
498
|
+
$ rails db:migrate
|
|
499
|
+
$ rails bunko:setup
|
|
500
|
+
$ rails bunko:sample_data
|
|
501
|
+
$ rails server
|
|
502
|
+
|
|
503
|
+
# Visit http://localhost:3000/blog and see working blog with sample content
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Out of Scope for 1.0
|
|
509
|
+
|
|
510
|
+
These are excellent features but not required for initial release:
|
|
511
|
+
|
|
512
|
+
- **Admin UI generator** - `rails generate bunko:admin`
|
|
513
|
+
- **Seed task** - `rails bunko:seed` for sample content
|
|
514
|
+
- **Custom fields DSL** - Beyond metadata jsonb
|
|
515
|
+
- **Publishing callbacks** - `after_publish`, etc.
|
|
516
|
+
- **Versioning support** - Draft history, rollback
|
|
517
|
+
- **Multi-collection controllers** - Single controller, many types
|
|
518
|
+
- **Author associations** - belongs_to :author
|
|
519
|
+
- **Category/tag models** - For now, use strings or metadata
|
data/Rakefile
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bunko
|
|
4
|
+
class Configuration
|
|
5
|
+
class PostTypeCustomizer
|
|
6
|
+
def initialize(post_type_hash)
|
|
7
|
+
@post_type_hash = post_type_hash
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def title=(value)
|
|
11
|
+
@post_type_hash[:title] = value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Future: Add more customization methods
|
|
15
|
+
# def per_page=(value)
|
|
16
|
+
# @post_type_hash[:per_page] = value
|
|
17
|
+
# end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class CollectionCustomizer
|
|
21
|
+
def initialize(collection_hash)
|
|
22
|
+
@collection_hash = collection_hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def title=(value)
|
|
26
|
+
@collection_hash[:title] = value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def post_types=(value)
|
|
30
|
+
# Normalize post_types to array of names (using underscores, not hyphens)
|
|
31
|
+
@collection_hash[:post_types] = Array(value).map { |pt| pt.to_s.parameterize.tr("-", "_") }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def scope=(callable)
|
|
35
|
+
@collection_hash[:scope] = callable
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Future: Add more customization methods
|
|
39
|
+
# def per_page=(value)
|
|
40
|
+
# @collection_hash[:per_page] = value
|
|
41
|
+
# end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_accessor :reading_speed, :excerpt_length, :auto_update_word_count, :valid_statuses, :post_types, :collections, :allow_static_pages
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
@reading_speed = 250 # words per minute
|
|
48
|
+
@excerpt_length = 160 # characters
|
|
49
|
+
@auto_update_word_count = true # automatically update word_count when content changes
|
|
50
|
+
@valid_statuses = %w[draft published scheduled]
|
|
51
|
+
@post_types = [] # Must be configured in initializer
|
|
52
|
+
@collections = [] # Multi-type collections
|
|
53
|
+
@allow_static_pages = true # Enable standalone pages feature by default
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def post_type(name, title: nil, &block)
|
|
57
|
+
# Validate name format (must use underscores, not hyphens, for Ruby class naming)
|
|
58
|
+
name_str = name.to_s
|
|
59
|
+
|
|
60
|
+
if name_str.include?("-")
|
|
61
|
+
raise ArgumentError, "PostType name '#{name_str}' cannot contain hyphens. Use underscores instead (e.g., 'case_study'). URLs will automatically use hyphens (/case-study/)."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
unless name_str.match?(/\A[a-z0-9_]+\z/)
|
|
65
|
+
raise ArgumentError, "PostType name '#{name_str}' must contain only lowercase letters, numbers, and underscores"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Reserved name for static pages feature
|
|
69
|
+
if name_str == "pages"
|
|
70
|
+
raise ArgumentError, "PostType name 'pages' is reserved for the static pages feature. Use config.allow_static_pages to control this feature."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check for conflicts with existing collections
|
|
74
|
+
if collection_exists?(name_str)
|
|
75
|
+
raise ArgumentError, "PostType name '#{name_str}' conflicts with existing collection name"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Auto-generate title from name (e.g., "case_study" → "Case Study")
|
|
79
|
+
generated_title = title || name_str.titleize
|
|
80
|
+
|
|
81
|
+
post_type = {name: name_str, title: generated_title}
|
|
82
|
+
|
|
83
|
+
# Allow customization via block (block overrides params)
|
|
84
|
+
if block_given?
|
|
85
|
+
customizer = PostTypeCustomizer.new(post_type)
|
|
86
|
+
block.call(customizer)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@post_types << post_type
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def collection(name, title: nil, post_types: nil, scope: nil, &block)
|
|
93
|
+
# Validate name format (must use underscores, not hyphens)
|
|
94
|
+
name_str = name.to_s
|
|
95
|
+
|
|
96
|
+
if name_str.include?("-")
|
|
97
|
+
raise ArgumentError, "Collection name '#{name_str}' cannot contain hyphens. Use underscores instead (e.g., 'long_reads'). URLs will automatically use hyphens (/long-reads/)."
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
unless name_str.match?(/\A[a-z0-9_]+\z/)
|
|
101
|
+
raise ArgumentError, "Collection name '#{name_str}' must contain only lowercase letters, numbers, and underscores"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check for conflicts with existing post_types
|
|
105
|
+
if post_type_exists?(name_str)
|
|
106
|
+
raise ArgumentError, "Collection name '#{name_str}' conflicts with existing PostType name"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check for conflicts with existing collections
|
|
110
|
+
if collection_exists?(name_str)
|
|
111
|
+
raise ArgumentError, "Collection '#{name_str}' already exists"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Require at least post_types param or block
|
|
115
|
+
unless post_types || block_given?
|
|
116
|
+
raise ArgumentError, "Collection '#{name_str}' requires either post_types parameter or a configuration block"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Auto-generate title from name (e.g., "long_reads" → "Long Reads")
|
|
120
|
+
generated_title = title || name_str.titleize
|
|
121
|
+
|
|
122
|
+
# Normalize post_types to array of names (using underscores, not hyphens)
|
|
123
|
+
normalized_post_types = post_types ? Array(post_types).map { |pt| pt.to_s.parameterize.tr("-", "_") } : []
|
|
124
|
+
|
|
125
|
+
collection = {
|
|
126
|
+
name: name_str,
|
|
127
|
+
title: generated_title,
|
|
128
|
+
post_types: normalized_post_types,
|
|
129
|
+
scope: scope
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Allow customization via block (block overrides params)
|
|
133
|
+
if block_given?
|
|
134
|
+
customizer = CollectionCustomizer.new(collection)
|
|
135
|
+
block.call(customizer)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Validate that post_types was set
|
|
139
|
+
if collection[:post_types].empty?
|
|
140
|
+
raise ArgumentError, "Collection '#{name_str}' must specify at least one post_type"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
@collections << collection
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def find_post_type(name)
|
|
147
|
+
@post_types.find { |pt| pt[:name] == name.to_s }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def find_collection(name)
|
|
151
|
+
@collections.find { |c| c[:name] == name.to_s }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def post_type_exists?(name)
|
|
157
|
+
@post_types.any? { |pt| pt[:name] == name.to_s }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def collection_exists?(name)
|
|
161
|
+
@collections.any? { |c| c[:name] == name.to_s }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class << self
|
|
166
|
+
attr_writer :configuration
|
|
167
|
+
|
|
168
|
+
def configuration
|
|
169
|
+
@configuration ||= Configuration.new
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def configure
|
|
173
|
+
yield(configuration)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def reset_configuration!
|
|
177
|
+
@configuration = Configuration.new
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|