Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ GEM
marcel (1.1.0)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.0)
minitest (5.27.0)
net-imap (0.5.12)
date
net-protocol
Expand Down Expand Up @@ -244,7 +244,7 @@ GEM
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
unicode-emoji (4.2.0)
uri (1.1.1)
useragent (0.16.11)
websocket-driver (0.8.0)
Expand Down
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,74 @@ end

**Note:** The `"pages"` post_type name is reserved for this feature. If you try to create a post_type named "pages", Bunko will raise an error.

### Avo Admin Panel Integration

Bunko provides a generator for creating an [Avo](https://avohq.io) admin panel with a clean editor layout.

**Prerequisites:**

1. Install Avo gem:
```ruby
# Gemfile
gem "avo"
```

2. Install and configure Avo:
```bash
bundle install
rails generate avo:install
```

**Generate Bunko Avo Resource:**

```bash
rails bunko:avo:install
```

This creates:
- `app/avo/resources/post.rb` - Post resource with main content area and metadata sidebar
- `app/avo/filters/post_type_filter.rb` - Filter posts by type
- `app/avo/actions/publish_post.rb` - Quick publish action
- `app/avo/actions/unpublish_post.rb` - Quick unpublish action

**Editor Options:**

Bunko defaults to Avo's markdown field (powered by [Marksmith](https://github.com/avo-hq/marksmith)), but supports multiple editors:

```bash
# Default: Markdown field (GitHub-style editor with Marksmith)
rails bunko:avo:install

# Rhino (TipTap-based WYSIWYG editor)
EDITOR=rhino rails bunko:avo:install

# TipTap (WYSIWYG editor)
EDITOR=tiptap rails bunko:avo:install

# Trix (Rails default rich text editor)
EDITOR=trix rails bunko:avo:install

# Plain textarea (simple text input)
EDITOR=textarea rails bunko:avo:install
```

**Required gems for rich editors:**

- **Markdown**: Add `gem "marksmith"` and `gem "commonmarker"` to your Gemfile
- **Rhino**: Add `gem "avo-rhino_field"` to your Gemfile
- **TipTap**: Add `gem "avo-tiptap_field"` to your Gemfile (if available)
- **Trix**: Built into Rails (no additional gems needed)

See the [Avo fields documentation](https://docs.avohq.io) for detailed setup instructions.

**Layout:**

The generated resource features:
- **Main content area:** Title and content editor
- **Sidebar:** Status, publishing options, post type, slug, SEO fields, content stats, and timestamps

Visit `http://localhost:3000/avo` to access your admin panel.

### Configuration

```ruby
Expand Down
133 changes: 133 additions & 0 deletions lib/tasks/bunko/avo.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# frozen_string_literal: true

require "fileutils"
require_relative "helpers"

namespace :bunko do
namespace :avo do
include Bunko::RakeHelpers

desc "Generate Avo resource for Bunko posts"
task install: :environment do
puts "Generating Avo resource for Bunko..."
puts ""

# Check if Avo is installed
begin
require "avo"
rescue LoadError
puts "⚠️ Avo gem not found!"
puts " Add 'gem \"avo\"' to your Gemfile and run 'bundle install'"
puts " Then run 'rails generate avo:install' before running this task"
exit 1
end

# Check if Avo has been initialized
avo_initializer = Rails.root.join("config/initializers/avo.rb")
unless File.exist?(avo_initializer)
puts "⚠️ Avo not initialized!"
puts " Run 'rails generate avo:install' first"
exit 1
end

# Get configured post types for dynamic filters
post_types = Bunko.configuration.post_types.map { |pt| pt[:name] }

# Detect editor preference (Avo-specific editors)
editor_type = ENV.fetch("EDITOR", "markdown") # Options: markdown (marksmith), rhino, tiptap, trix, textarea

# Generate Avo resource, filters, and actions
generate_avo_post_resource(post_types, editor_type)
generate_avo_post_type_filter(post_types) if post_types.any?
generate_avo_actions

puts "=" * 79
puts "Avo resource generated!"
puts ""
puts "Next steps:"
puts " 1. Visit http://localhost:3000/avo to access your admin panel"
puts " 2. Customize app/avo/resources/post.rb as needed"
puts ""
puts "Editor type: #{editor_type}"
puts " To change, run: EDITOR=rhino rails bunko:avo:install"
puts " Options: markdown (default, uses Marksmith), rhino, tiptap, trix, textarea"
puts "=" * 79
end

private

def generate_avo_post_resource(post_types, editor_type)
resources_dir = Rails.root.join("app/avo/resources")
resource_file = resources_dir.join("post.rb")

if File.exist?(resource_file)
puts " - app/avo/resources/post.rb already exists (skipped)"
puts " To regenerate, delete the file and run again"
return false
end

FileUtils.mkdir_p(resources_dir)

resource_content = render_template(
"avo/resources/post.rb.tt",
post_types: post_types,
editor_type: editor_type
)

File.write(resource_file, resource_content)

puts " ✓ Created app/avo/resources/post.rb"
puts " - Main content area with #{editor_type} editor"
puts " - Metadata sidebar with publishing, SEO, and stats"
puts " - Post type filters: #{post_types.join(", ")}" if post_types.any?
true
end

def generate_avo_post_type_filter(post_types)
filters_dir = Rails.root.join("app/avo/filters")
filter_file = filters_dir.join("post_type_filter.rb")

if File.exist?(filter_file)
puts " - app/avo/filters/post_type_filter.rb already exists (skipped)"
return false
end

FileUtils.mkdir_p(filters_dir)

filter_content = render_template(
"avo/filters/post_type_filter.rb.tt",
post_types: post_types
)

File.write(filter_file, filter_content)

puts " ✓ Created app/avo/filters/post_type_filter.rb"
true
end

def generate_avo_actions
actions_dir = Rails.root.join("app/avo/actions")
FileUtils.mkdir_p(actions_dir)

# Generate PublishPost action
publish_action = actions_dir.join("publish_post.rb")
if File.exist?(publish_action)
puts " - app/avo/actions/publish_post.rb already exists (skipped)"
else
publish_content = render_template("avo/actions/publish_post.rb.tt", {})
File.write(publish_action, publish_content)
puts " ✓ Created app/avo/actions/publish_post.rb"
end

# Generate UnpublishPost action
unpublish_action = actions_dir.join("unpublish_post.rb")
if File.exist?(unpublish_action)
puts " - app/avo/actions/unpublish_post.rb already exists (skipped)"
else
unpublish_content = render_template("avo/actions/unpublish_post.rb.tt", {})
File.write(unpublish_action, unpublish_content)
puts " ✓ Created app/avo/actions/unpublish_post.rb"
end
end
end
end
14 changes: 7 additions & 7 deletions lib/tasks/bunko/install.rake
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ namespace :bunko do

# Step 1: Create migrations
puts "Creating migrations..."
create_post_types_migration(skip_seo: skip_seo, json_content: json_content)
sleep 1 # Ensure different timestamps
create_posts_migration(skip_seo: skip_seo, json_content: json_content)
base_time = Time.now.utc
timestamp1 = base_time.strftime("%Y%m%d%H%M%S")
timestamp2 = (base_time + 1).strftime("%Y%m%d%H%M%S")
create_post_types_migration(timestamp: timestamp1, skip_seo: skip_seo, json_content: json_content)
create_posts_migration(timestamp: timestamp2, skip_seo: skip_seo, json_content: json_content)
puts ""

# Step 2: Create models
Expand All @@ -38,8 +40,7 @@ namespace :bunko do

# Helper methods

def create_post_types_migration(skip_seo:, json_content:)
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
def create_post_types_migration(timestamp:, skip_seo:, json_content:)
migration_file = Rails.root.join("db/migrate/#{timestamp}_create_post_types.rb")

if Dir.glob(Rails.root.join("db/migrate/*_create_post_types.rb")).any?
Expand All @@ -57,8 +58,7 @@ namespace :bunko do
true
end

def create_posts_migration(skip_seo:, json_content:)
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
def create_posts_migration(timestamp:, skip_seo:, json_content:)
migration_file = Rails.root.join("db/migrate/#{timestamp}_create_posts.rb")

if Dir.glob(Rails.root.join("db/migrate/*_create_posts.rb")).any?
Expand Down
32 changes: 28 additions & 4 deletions lib/tasks/bunko/sample_data.rake
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,12 @@ namespace :bunko do

# Add routes for the created pages
pages_to_create.each do |page_def|
# Home gets special treatment with path: "/"
# Home gets special treatment - add as root route
if page_def[:slug] == "home"
if add_bunko_page_route(page_def[:slug], path: "/")
puts " ✓ Added route: bunko_page :home, path: \"/\""
if add_root_route(page_def[:slug])
puts " ✓ Added route: root \"pages#show\", defaults: { page: \"home\" }"
else
puts " - Route for :home already exists (skipped)"
puts " - Root route already exists (skipped)"
end
elsif add_bunko_page_route(page_def[:slug])
puts " ✓ Added route: bunko_page :#{page_def[:slug].tr("-", "_")}"
Expand Down Expand Up @@ -240,6 +240,30 @@ namespace :bunko do
Rails.application.routes.named_routes[:root].present?
end

def add_root_route(page_slug)
routes_file = Rails.root.join("config/routes.rb")
routes_content = File.read(routes_file)

# Check if root route already exists
if routes_content.match?(/^\s*root\s/)
return false
end

# Find the Rails.application.routes.draw block start
if routes_content.match?(/Rails\.application\.routes\.draw do/)
# Insert root route right after the 'do' line
updated_content = routes_content.sub(
/(Rails\.application\.routes\.draw do\n)/,
"\\1 root \"pages#show\", defaults: {page: \"#{page_slug}\"}\n\n"
)
else
return false
end

File.write(routes_file, updated_content)
true
end

def add_bunko_page_route(slug, path: nil)
routes_file = Rails.root.join("config/routes.rb")
routes_content = File.read(routes_file)
Expand Down
19 changes: 19 additions & 0 deletions lib/tasks/templates/avo/actions/publish_post.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class Avo::Actions::PublishPost < Avo::BaseAction
self.name = "Publish"
self.visible = -> { record.status != "published" }
self.message = "Are you sure you want to publish this post?"
self.confirm_button_label = "Publish"

def handle(**args)
args[:records].each do |post|
# Updates status to published
# Note: published_at is auto-set by Bunko callback if blank
# If published_at is already set (e.g. scheduled post), it will be preserved
post.update(status: "published")
end

succeed "Post(s) published successfully!"
end
end
19 changes: 19 additions & 0 deletions lib/tasks/templates/avo/actions/unpublish_post.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class Avo::Actions::UnpublishPost < Avo::BaseAction
self.name = "Unpublish"
self.visible = -> { record.status == "published" }
self.message = "Are you sure you want to unpublish this post?"
self.confirm_button_label = "Unpublish"

def handle(**args)
args[:records].each do |post|
# Updates status to draft
# Note: published_at is preserved for re-publishing with original date
# To reset published_at, clear it manually before re-publishing
post.update(status: "draft")
end

succeed "Post(s) unpublished successfully!"
end
end
23 changes: 23 additions & 0 deletions lib/tasks/templates/avo/filters/post_type_filter.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class Avo::Filters::PostTypeFilter < Avo::Filters::SelectFilter
self.name = "Post Type"
self.visible = -> { true }

def apply(request, query, value)
return query if value.blank?

post_type = PostType.find_by(name: value)
return query if post_type.nil?

query.by_post_type(post_type)
end

def options
{
<% post_types.each_with_index do |pt, index| -%>
"<%= pt.titleize %>" => "<%= pt %>"<%= index < post_types.length - 1 ? "," : "" %>
<% end -%>
}
end
end
Loading
Loading