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.
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.
- Milestone 1: Post Model Behavior - ✅ COMPLETED
- Milestone 2: Collection Controllers - ✅ COMPLETED
- Milestone 3: Installation Generator - ✅ COMPLETED
- Milestone 4: Routing Helpers - ✅ COMPLETED
- Milestone 5: Post Convenience Methods - ✅ COMPLETED
- Milestone 6: Static Pages - ✅ COMPLETED
- Milestone 7: Configuration - 🚧 PENDING (core system exists, may need expansion)
- Milestone 8: Documentation - 🚧 PENDING
- Milestone 9: Release - 🚧 PENDING
By 1.0, a Rails developer should be able to:
- ✅ Install Bunko and generate a working blog in < 5 minutes
- ✅ Add a second content collection (e.g.,
/docs) in < 2 minutes - ✅ Attach the text editor of their choice to create and edit posts
- ✅ Schedule posts for future publication
- ✅ Organize content into different post types without migrations
- ✅ Customize views with their own HTML/CSS
- ✅ Automatically use slug-based URLs instead of IDs
- ✅ Create standalone pages (About, Contact, etc.) without full collections
Spec: A Post model with Bunko enabled should have all essential CMS functionality.
Scopes & Queries:
- Developer can query
Post.publishedand only see published posts withpublished_at <= Time.current - Developer can query
Post.draftand only see draft posts - Developer can query
Post.scheduledand only see posts scheduled for future publication - Developer can filter posts by type:
Post.by_post_type('blog')or similar API - Default ordering shows most recent posts first
Slug Generation:
- When a post is created without a slug, one is auto-generated from the title
- Slugs are URL-safe (e.g., "Hello World" becomes "hello-world")
- Slugs are unique within their post_type
- Developer can provide custom slug and it won't be overwritten
Publishing Workflow:
- Post status can be: 'draft', 'published', or 'scheduled'
- When status changes to 'published' and
published_atis blank, it auto-sets to current time - Posts with status='published' but
published_atin future are treated as scheduled - Invalid status values are rejected
Reading Metrics:
- If post has
word_count, developer can get estimated reading time - Reading time calculation is configurable (default ~250 words/minute)
Routing Support:
- 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:
- resources :case_studies, controller: 'case_studies', path: 'case-studies', param: :slug, only: %i[index show]
- 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 or Markdown editor, they don't need to mount this section at all. https://avohq.io/
require "bunko/editor" # require the web UI
Rails.application.routes.draw do
mount Bunko::Editor => "/posts" # access it at http://localhost:3000/posts
...
end
# Developer can do this:
class Post < ApplicationRecord
acts_as_bunko_post
end
# And get this behavior:
post = Post.create!(title: "Hello World", content: "...", post_type: 'blog')
post.slug # => "hello-world"
post.to_param # => "hello-world"
post.status # => "draft"
post.update!(status: 'published')
post.published_at # => Time.current (auto-set)
Post.published.count # => 1
Post.draft.count # => 0Spec: A controller should be able to serve a content collection with minimal code.
Defaults
- If user just adds a routes, all of our standard behaviors should be observed.
- If user chooses to generate their own controller, we should allow that.
- If user wants to use our controllers but adjust settings through bunko.rb initializer, we should allow that.
Index Action:
- Shows all published posts for a given post_type
- Does not leak other configured posts types in any way or scopes
- Paginates results - recommend pagy but this should be adaptable? Or perhaps enable Pagy or kamanari behavior in a bunko.rb initializer
- Allows customization of per_page, ordering, layout
- Provides access to collection name in views
Show Action:
- Finds post by slug
- Scoped to the correct - eg you should be able to have same slug on 2 different post types
- Returns 404 if not found (and routes doesn't take over with a 301 or something)
- Provides access to the post in views
Multiple Post Types:
- Single controller can serve multiple related post types
- Example: Resources controller serves guides, templates, checklists
# Developer can do this:
class BlogController < ApplicationController
bunko_collection :blog # or whatever the API is
end
# And get these routes working:
GET /blog # => BlogController#index (lists blog posts)
GET /blog/:slug # => BlogController#show (shows single post)
# Views have access to:
@posts # in index
@post # in show
@collection_name # 'blog'
# Developer can customize:
class ChangelogController < ApplicationController
bunko_collection :changelog, per_page: 20, layout: 'docs'
endSpec: Running rails bunko:install should create everything needed for a working blog.
Implementation Note: This milestone was implemented as a two-phase pattern:
rails bunko:install- Creates migrations, models, and initializerrails bunko:setup- Generates controllers, views, and routes based on configuration
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.
Migration Creation:
- Detects database type (PostgreSQL, SQLite, MySQL) and generates appropriate migration
- Creates
poststable with essential fields:title(string, required)slug(string, required, indexed)content(text by default, json/jsonb with--json-contentflag for JSON-based editors)post_type(references, required)status(string, indexed, default: 'draft') # should this be references to a post_status table?published_at(datetime, indexed)- Timestamps for created_at and updated_at
- Creates
post_typestable with essential fields:name(string, required)slug(string, required, indexed)- Timestamps
- Adds unique constraint on
[slug, post_type] - 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?
- Optional fields based on flags (see Generator Options below)
- User should be able to override our migration and add something like "acts_as_post" to the model to get the same behaviors.
Model Generation:
- Creates
app/models/post.rbwith Bunko enabled - Creates
app/models/post_type.rbwith Bunko enabled - Includes comments explaining customization
Controller Generation:
- Creates
app/controllers/blog_controller.rb - Configured to serve 'blog' post type
- Includes comments explaining how to add more collections
View Generation:
- Creates
app/views/blog/index.html.erbwith semantic HTML - Creates
app/views/blog/show.html.erbwith semantic HTML - No CSS, no JavaScript - just clean HTML with helpful comments
- Shows title, content, published date, reading time
Route Generation:
- Adds routes for blog (index + show)
- Uses slug as param, not ID
Initializer Generation:
- Creates
config/initializers/bunko.rb - Includes common configuration options (commented out with examples)
Post-Install Message:
- Shows next steps (run migration, create first post)
- Links to documentation
--skip-seo- Skip adding SEO fields (title_tag, meta_description)--json-content- Use json/jsonb for content field instead of text (for JSON-based editors)
# Developer runs:
$ rails bunko:install
$ rails db:migrate
# Result: They can visit /blog and see a working (empty) blog
# They can create a post in console and see it at /blog/post-slugSpec: Setting up routes for collections should be simple and conventional.
Route DSL:
- Developer can call
bunko_collection :bloginstead of writing full resources line - Supports custom paths (e.g.,
bunko_collection :case_study, path: 'case-studies') - Supports limiting actions (e.g.,
only: [:index]for index-only collection) # not critical - Supports custom controller names
# Developer can do this in config/routes.rb:
Rails.application.routes.draw do
bunko_collection :blog
bunko_collection :docs
bunko_collection :case_study, path: 'case-studies' #perhaps path should be automatically hyphenated and/or inflected/pluralized?
end
# And get these routes:
# /blog => blog#index
# /blog/:slug => blog#show
# /docs => docs#index
# /docs/:slug => docs#show
# /case-studies => case_study#index
# /case-studies/:slug => case_study#showSpec: Common CMS view patterns should be available as Post instance methods for clean, conflict-free usage in views.
Implementation Note: Originally planned as view helpers, we decided to implement these as Post model methods instead. This approach:
- Avoids namespace conflicts (no
bunko_prefix needed for generic helper names) - Keeps views cleaner (
post.excerptvsbunko_excerpt(post)) - Works identically in index loops and show views
Content Formatting:
post.excerpt(length: 160, omission: "...")- returns truncated content, strips HTML, preserves word boundariespost.reading_time_text- returns "X min read" string (extends existingreading_timeinteger method)
Date Formatting:
post.published_date(format = :long)- returns formatted published_at using I18n.l- Supports Rails date formats:
:long,:short,:db, custom strftime
Navigation:
- Not needed - routing DSL automatically generates helpers like
blog_path,blog_post_path(post)
<!-- Index view: loop over posts -->
<% @posts.each do |post| %>
<article>
<h2><%= link_to post.title, blog_post_path(post) %></h2>
<p class="meta">
<%= post.published_date %> · <%= post.reading_time_text %>
</p>
<p><%= post.excerpt %></p>
</article>
<% end %>
<!-- Show view: single post -->
<article>
<h1><%= @post.title %></h1>
<p class="meta">
<%= @post.published_date(:long) %> · <%= @post.reading_time_text %>
</p>
<div class="content">
<%= @post.content %>
</div>
</article>Spec: Developers should be able to create standalone pages (About, Contact, Privacy Policy) without needing a full collection with an index page.
Routing DSL:
bunko_page :aboutcreates a single route:GET /about → pages#show- Supports custom paths:
bunko_page :about, path: "about-us" - Supports custom controllers:
bunko_page :contact, controller: "static_pages" - Works with namespaces:
namespace :legal do bunko_page :privacy end
PagesController:
- Single shared controller for all pages (no per-page controller generation)
- Smart view resolution: checks for custom page template (e.g.,
pages/about.html.erb) first - Falls back to default
pages/show.html.erbif custom template doesn't exist - Raises 404 if page Post not found
Configuration:
- Opt-out support:
config.allow_static_pages = false - Reserved "pages" post_type namespace with validation error
- Auto-generated during
rails bunko:setupif enabled (default: true)
Architecture:
- Uses same Post model as collections (maintains one-model architecture)
- Pages stored with
post_type = "pages" - Slug must match route name (e.g., route
bunko_page :aboutexpects Post with slug "about")
# In config/routes.rb:
Rails.application.routes.draw do
bunko_page :about
bunko_page :contact
bunko_page :privacy
end
# Create page content:
pages_type = PostType.find_by(name: "pages")
Post.create!(
title: "About Us",
content: "<p>Welcome to our company...</p>",
post_type: pages_type,
slug: "about", # Must match route name
status: "published"
)
# Result:
# GET /about → renders pages/about.html.erb or pages/show.html.erb
# GET /contact → 404 (no Post with slug "contact" exists yet)Spec: Bunko behavior should be customizable via initializer without modifying gem code.
Configurable Options:
- Post model name (default: 'Post')
- Valid post types (default: ['post'])
- Valid statuses (default: ['draft', 'published', 'scheduled'])
- Default status (default: 'draft')
- Reading speed in words/minute (default: 250)
- Excerpt length (default: 160)
- Slug generation strategy (default: parameterize)
Configuration API:
- Developer uses block syntax in initializer
- Configuration is globally accessible
- Invalid configuration values are validated
# Developer can configure in config/initializers/bunko.rb:
Bunko.configure do |config|
config.post_type "post"
config.post_type "page"
config.post_type "doc"
config.post_type "tutorial"
config.reading_speed = 200
config.excerpt_length = 200
config.slug_generator = ->(title) { title.parameterize.truncate(50) }
end
# And it affects behavior:
post = Post.create!(title: "Very Long Title...")
# slug uses custom generatorSpec: Documentation should be excellent, examples should be practical.
README.md:
- Philosophy and goals clearly stated
- Installation instructions (add to Gemfile, run generator)
- Quick start guide (5 minute blog)
- Multi-collection setup example
- Configuration options documented
- Customization patterns explained
- What Bunko doesn't do (auth, admin UI, etc.)
EXAMPLES.md:
- Basic blog setup
- Blog + docs setup
- Custom fields using metadata
- Custom scopes
- Overriding views
- Integration with admin gems (Avo, Administrate, etc.)
Code Documentation:
- All public APIs have clear documentation
- Generated code includes helpful comments
- Configuration options explained in generated initializer
Example Apps:
examples/basic_blog- Minimal blogexamples/multi_collection- Blog + docs + changelog
New developer can:
- Read README and understand what Bunko does in < 2 minutes
- Follow quick start and have working blog in < 5 minutes
- Find answer to "how do I customize X?" in documentation
- Clone an example app and see Bunko in action
Spec: 1.0.0 is published to RubyGems and ready for production use.
Compatibility:
- Works with Rails 8.0+ and follows Rails EOL maintenance policy
- Works with Ruby 3.2, 3.3, 3.4 and follows Ruby EOL maintenance policy
- Works with PostgreSQL, SQLite, MySQL
- Test coverage > 90%
- All Standard linter checks pass
Package:
- bunko.gemspec has no TODOs
- Proper description and summary
- Correct homepage and source URLs
- Appropriate version number (1.0.0)
- CHANGELOG.md updated
Documentation:
- README complete and accurate
- EXAMPLES.md or docs have practical examples
- Generated code has helpful comments
- GitHub release notes written
Distribution:
- Gem builds successfully
- Gem published to RubyGems
- GitHub release created
- Installation tested in fresh Rails app
# Any developer can:
$ gem install bunko
$ rails new myblog
$ cd myblog
$ bundle add bunko
$ rails bunko:install
$ rails db:migrate
$ rails bunko:setup
$ rails bunko:sample_data
$ rails server
# Visit http://localhost:3000/blog and see working blog with sample contentThese are excellent features but not required for initial release:
- Admin UI generator -
rails generate bunko:admin - Seed task -
rails bunko:seedfor sample content - Custom fields DSL - Beyond metadata jsonb
- Publishing callbacks -
after_publish, etc. - Versioning support - Draft history, rollback
- Multi-collection controllers - Single controller, many types
- Author associations - belongs_to :author
- Category/tag models - For now, use strings or metadata