# 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 LINT_TARGETS = %w[bake.rb Gemfile lib spec].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 " 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(*standardrb_command) end # Auto-fix StandardRB issues def lint_fix exec(*standardrb_command("--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 standardrb_command(*extra_args) ["bundle", "exec", "standardrb", *extra_args, *LINT_TARGETS] 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+/, "-").squeeze("-") .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 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