mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
279 lines
6.7 KiB
Ruby
279 lines
6.7 KiB
Ruby
# Build tasks for samhuri.net static site generator
|
|
|
|
require 'etc'
|
|
require 'fileutils'
|
|
|
|
DRAFTS_DIR = 'public/drafts'.freeze
|
|
PUBLISH_HOST = 'mudge'.freeze
|
|
PRODUCTION_PUBLISH_DIR = '/var/www/samhuri.net/public'.freeze
|
|
BETA_PUBLISH_DIR = '/var/www/beta.samhuri.net/public'.freeze
|
|
WATCHABLE_DIRECTORIES = %w[public posts lib].freeze
|
|
BUILD_TARGETS = %w[debug mudge beta release].freeze
|
|
|
|
# Generate the site in debug mode (localhost:8000)
|
|
def debug
|
|
build('http://localhost:8000')
|
|
end
|
|
|
|
# Generate the site for the mudge development server
|
|
def mudge
|
|
build('http://mudge:8000')
|
|
end
|
|
|
|
# Generate the site for beta/staging
|
|
def beta
|
|
build('https://beta.samhuri.net')
|
|
end
|
|
|
|
# Generate the site for production
|
|
def release
|
|
build('https://samhuri.net')
|
|
end
|
|
|
|
# Start local development server
|
|
def serve
|
|
require 'webrick'
|
|
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: 'www')
|
|
trap('INT') { server.shutdown }
|
|
puts 'Server running at http://localhost:8000'
|
|
server.start
|
|
end
|
|
|
|
# Create a new draft in public/drafts/.
|
|
# @parameter title_parts [Array] Optional title words; defaults to Untitled.
|
|
def new_draft(*title_parts)
|
|
title, filename =
|
|
if title_parts.empty?
|
|
['Untitled', next_available_draft]
|
|
else
|
|
given_title = title_parts.join(' ')
|
|
slug = slugify(given_title)
|
|
abort 'Error: title cannot be converted to a filename.' if slug.empty?
|
|
|
|
filename = "#{slug}.md"
|
|
path = draft_path(filename)
|
|
abort "Error: draft already exists at #{path}" if File.exist?(path)
|
|
|
|
[given_title, filename]
|
|
end
|
|
|
|
FileUtils.mkdir_p(DRAFTS_DIR)
|
|
path = draft_path(filename)
|
|
content = render_draft_template(title)
|
|
File.write(path, content)
|
|
|
|
puts "Created new draft at #{path}"
|
|
puts '>>> Contents below <<<'
|
|
puts
|
|
puts content
|
|
end
|
|
|
|
# Publish a draft by moving it to posts/YYYY/MM and updating dates.
|
|
# @parameter input_path [String] Draft path or filename in public/drafts.
|
|
def publish_draft(input_path = nil)
|
|
if input_path.nil? || input_path.strip.empty?
|
|
puts 'Usage: bake publish_draft <draft-path-or-filename>'
|
|
puts
|
|
puts 'Available drafts:'
|
|
drafts = Dir.glob("#{DRAFTS_DIR}/*.md").map { |path| File.basename(path) }
|
|
if drafts.empty?
|
|
puts ' (no drafts found)'
|
|
else
|
|
drafts.each { |draft| puts " #{draft}" }
|
|
end
|
|
abort
|
|
end
|
|
|
|
draft_path_value, draft_file = resolve_draft_input(input_path)
|
|
abort "Error: File not found: #{draft_path_value}" unless File.exist?(draft_path_value)
|
|
|
|
now = Time.now
|
|
content = File.read(draft_path_value)
|
|
content.sub!(/^Date:.*$/, "Date: #{ordinal_date(now)}")
|
|
content.sub!(/^Timestamp:.*$/, "Timestamp: #{now.strftime('%Y-%m-%dT%H:%M:%S%:z')}")
|
|
|
|
target_dir = "posts/#{now.strftime('%Y/%m')}"
|
|
FileUtils.mkdir_p(target_dir)
|
|
target_path = "#{target_dir}/#{draft_file}"
|
|
|
|
File.write(target_path, content)
|
|
FileUtils.rm_f(draft_path_value)
|
|
|
|
puts "Published draft: #{draft_path_value} -> #{target_path}"
|
|
end
|
|
|
|
# Watch content directories and rebuild on every change.
|
|
# @parameter target [String] One of debug, mudge, beta, or release.
|
|
def watch(target: 'debug')
|
|
unless command_available?('inotifywait')
|
|
abort 'inotifywait is required (install inotify-tools).'
|
|
end
|
|
|
|
loop do
|
|
abort 'Error: watch failed.' unless system('inotifywait', '-e', 'modify,create,delete,move', *watch_paths)
|
|
puts "changed at #{Time.now}"
|
|
sleep 2
|
|
run_build_target(target)
|
|
end
|
|
end
|
|
|
|
# Publish to beta/staging server
|
|
def publish_beta
|
|
beta
|
|
run_rsync(local_paths: ['www/'], publish_dir: BETA_PUBLISH_DIR, dry_run: false, delete: true)
|
|
end
|
|
|
|
# Publish to production server
|
|
def publish
|
|
release
|
|
run_rsync(local_paths: ['www/'], publish_dir: PRODUCTION_PUBLISH_DIR, dry_run: false, delete: true)
|
|
end
|
|
|
|
# Clean generated files
|
|
def clean
|
|
FileUtils.rm_rf('www')
|
|
puts 'Cleaned www/ directory'
|
|
end
|
|
|
|
# Run RSpec tests
|
|
def test
|
|
exec 'bundle exec rspec'
|
|
end
|
|
|
|
# Run Guard for continuous testing
|
|
def guard
|
|
exec 'bundle exec guard'
|
|
end
|
|
|
|
# List all available drafts
|
|
def drafts
|
|
Dir.glob("#{DRAFTS_DIR}/*.md").sort.each do |draft|
|
|
puts File.basename(draft)
|
|
end
|
|
end
|
|
|
|
# Run StandardRB linter
|
|
def lint
|
|
exec 'bundle exec standardrb'
|
|
end
|
|
|
|
# Auto-fix StandardRB issues
|
|
def lint_fix
|
|
exec 'bundle exec standardrb --fix'
|
|
end
|
|
|
|
private
|
|
|
|
# Build the site with specified URL
|
|
# @parameter url [String] The site URL to use
|
|
def build(url)
|
|
require_relative 'lib/pressa'
|
|
|
|
puts "Building site for #{url}..."
|
|
site = Pressa.create_site(source_path: '.', url_override: url)
|
|
generator = Pressa::SiteGenerator.new(site:)
|
|
generator.generate(source_path: '.', target_path: 'www')
|
|
puts 'Site built successfully in www/'
|
|
end
|
|
|
|
def run_build_target(target)
|
|
target_name = target.to_s
|
|
unless BUILD_TARGETS.include?(target_name)
|
|
abort "Error: invalid target '#{target_name}'. Use one of: #{BUILD_TARGETS.join(', ')}"
|
|
end
|
|
|
|
public_send(target_name)
|
|
end
|
|
|
|
def watch_paths
|
|
WATCHABLE_DIRECTORIES.flat_map { |path| ['-r', path] }
|
|
end
|
|
|
|
def run_rsync(local_paths:, publish_dir:, dry_run:, delete:)
|
|
command = ['rsync', '-aKv', '-e', 'ssh -4']
|
|
command << '--dry-run' if dry_run
|
|
command << '--delete' if delete
|
|
command.concat(local_paths)
|
|
command << "#{PUBLISH_HOST}:#{publish_dir}"
|
|
abort 'Error: rsync failed.' unless system(*command)
|
|
end
|
|
|
|
def resolve_draft_input(input_path)
|
|
if input_path.include?('/')
|
|
if input_path.start_with?('posts/')
|
|
abort "Error: '#{input_path}' is already published in posts/ directory"
|
|
end
|
|
|
|
[input_path, File.basename(input_path)]
|
|
else
|
|
[draft_path(input_path), input_path]
|
|
end
|
|
end
|
|
|
|
def draft_path(filename)
|
|
File.join(DRAFTS_DIR, filename)
|
|
end
|
|
|
|
def slugify(title)
|
|
title.downcase
|
|
.gsub(/[^a-z0-9\s-]/, '')
|
|
.gsub(/\s+/, '-')
|
|
.gsub(/-+/, '-')
|
|
.gsub(/^-|-$/, '')
|
|
end
|
|
|
|
def next_available_draft(base_filename = 'untitled.md')
|
|
return base_filename unless File.exist?(draft_path(base_filename))
|
|
|
|
name_without_ext = File.basename(base_filename, '.md')
|
|
counter = 1
|
|
loop do
|
|
numbered_filename = "#{name_without_ext}-#{counter}.md"
|
|
return numbered_filename unless File.exist?(draft_path(numbered_filename))
|
|
|
|
counter += 1
|
|
end
|
|
end
|
|
|
|
def render_draft_template(title)
|
|
now = Time.now
|
|
<<~FRONTMATTER
|
|
---
|
|
Author: #{current_author}
|
|
Title: #{title}
|
|
Date: unpublished
|
|
Timestamp: #{now.strftime('%Y-%m-%dT%H:%M:%S%:z')}
|
|
Tags:
|
|
---
|
|
|
|
# #{title}
|
|
|
|
TKTK
|
|
FRONTMATTER
|
|
end
|
|
|
|
def current_author
|
|
Etc.getlogin || ENV['USER'] || `whoami`.strip
|
|
rescue StandardError
|
|
ENV['USER'] || `whoami`.strip
|
|
end
|
|
|
|
def ordinal_date(time)
|
|
day = time.day
|
|
suffix = case day
|
|
when 1, 21, 31
|
|
'st'
|
|
when 2, 22
|
|
'nd'
|
|
when 3, 23
|
|
'rd'
|
|
else
|
|
'th'
|
|
end
|
|
|
|
time.strftime("#{day}#{suffix} %B, %Y")
|
|
end
|
|
|
|
def command_available?(command)
|
|
system('which', command, out: File::NULL, err: File::NULL)
|
|
end
|