Add dynamic drafts index page listing unpublished drafts

This commit is contained in:
Sami Samhuri 2026-06-21 20:10:22 -07:00
parent 93be8ad37d
commit 65e9734b8e
8 changed files with 324 additions and 0 deletions

View file

@ -0,0 +1,13 @@
require "dry-struct"
require "pressa/site"
module Pressa
class Drafts
class Entry < Dry::Struct
attribute :slug, Types::String
attribute :title, Types::String
attribute :timestamp, Types::Params::DateTime
attribute :path, Types::String
end
end
end

48
lib/pressa/drafts/repo.rb Normal file
View file

@ -0,0 +1,48 @@
require "yaml"
require "date"
require "pressa/drafts"
require "pressa/drafts/entry"
module Pressa
class Drafts
class Repo
def initialize(dir:)
@dir = dir
end
def read_entries
Dir.glob(File.join(@dir, "*.md")).sort.filter_map { |file_path| read_entry(file_path) }
.sort_by(&:timestamp)
.reverse
end
private
def read_entry(file_path)
content = File.read(file_path)
return nil unless content =~ /\A---\s*\n(.*?)\n---\s*\n/m
metadata = YAML.safe_load($1, permitted_classes: [Date, Time]) || {}
title = metadata["Title"]
timestamp_value = metadata["Timestamp"]
return nil unless title && timestamp_value
slug = File.basename(file_path, ".md")
timestamp = parse_timestamp(timestamp_value)
Entry.new(slug:, title:, timestamp:, path: "/drafts/#{slug}/")
end
def parse_timestamp(value)
case value
when String
DateTime.parse(value)
when Integer
Time.at(value).to_datetime
else
value.to_datetime
end
end
end
end
end

View file

@ -0,0 +1,30 @@
require "pressa/utils/file_writer"
require "pressa/views/layout"
require "pressa/views/drafts_view"
require "pressa/drafts"
module Pressa
class Drafts
class Writer
def initialize(site:, entries:)
@site = site
@entries = entries
end
def write_index(target_path:)
content_view = Views::DraftsView.new(entries: @entries, site: @site)
layout = Views::Layout.new(
site: @site,
page_subtitle: "Drafts",
canonical_url: @site.url_for("/drafts/"),
page_description: "Unpublished drafts",
content: content_view
)
file_path = File.join(target_path, "drafts", "index.html")
Utils::FileWriter.write(path: file_path, content: layout.call)
end
end
end
end

View file

@ -1,5 +1,7 @@
require "fileutils"
require "pressa/utils/file_writer"
require "pressa/drafts/repo"
require "pressa/drafts/writer"
module Pressa
class SiteGenerator
@ -23,10 +25,19 @@ module Pressa
copy_static_files(source_path, target_path)
process_public_directory(source_path, target_path)
write_drafts_index(source_path, target_path) if site.output_format == "html"
end
private
def write_drafts_index(source_path, target_path)
drafts_dir = File.join(source_path, "public", "drafts")
return unless Dir.exist?(drafts_dir)
entries = Drafts::Repo.new(dir: drafts_dir).read_entries
Drafts::Writer.new(site:, entries:).write_index(target_path:)
end
def validate_paths!(source_path:, target_path:)
source_abs = absolute_path(source_path)
target_abs = absolute_path(target_path)

View file

@ -0,0 +1,37 @@
require "phlex"
module Pressa
module Views
class DraftsView < Phlex::HTML
def initialize(entries:, site:)
@entries = entries
@site = site
end
def view_template
div(class: "container") do
h1 { "Drafts" }
ul(class: "posts") do
@entries.each do |entry|
render_entry(entry)
end
end
end
end
private
def render_entry(entry)
li do
a(href: entry.path) { entry.title }
time { short_date(entry.timestamp) }
end
end
def short_date(timestamp)
timestamp.strftime("%-d %b %Y")
end
end
end
end

89
test/drafts/repo_test.rb Normal file
View file

@ -0,0 +1,89 @@
require "test_helper"
require "fileutils"
require "tmpdir"
class Pressa::Drafts::RepoTest < Minitest::Test
def test_read_entries_parses_title_and_timestamp
Dir.mktmpdir do |dir|
File.write(File.join(dir, "powder-day.md"), <<~MARKDOWN)
---
Author: Shaun White
Title: Powder Day at Baker
Date: unpublished
Timestamp: 2025-11-05T10:00:00-08:00
Tags:
---
TKTK
MARKDOWN
entries = Pressa::Drafts::Repo.new(dir:).read_entries
assert_equal(1, entries.length)
entry = entries.first
assert_equal("powder-day", entry.slug)
assert_equal("Powder Day at Baker", entry.title)
assert_equal("/drafts/powder-day/", entry.path)
assert_equal(2025, entry.timestamp.year)
end
end
def test_read_entries_handles_legacy_unix_timestamps
Dir.mktmpdir do |dir|
File.write(File.join(dir, "old-draft.md"), <<~MARKDOWN)
---
Author: Greg Graffin
Title: Old Draft
Date: unpublished
Timestamp: 1435424525
---
TKTK
MARKDOWN
entry = Pressa::Drafts::Repo.new(dir:).read_entries.first
assert_equal(2015, entry.timestamp.year)
end
end
def test_read_entries_sorts_newest_first
Dir.mktmpdir do |dir|
File.write(File.join(dir, "older.md"), <<~MARKDOWN)
---
Author: Fat Mike
Title: Older Draft
Date: unpublished
Timestamp: 2024-01-01T00:00:00-08:00
---
TKTK
MARKDOWN
File.write(File.join(dir, "newer.md"), <<~MARKDOWN)
---
Author: El Hefe
Title: Newer Draft
Date: unpublished
Timestamp: 2025-01-01T00:00:00-08:00
---
TKTK
MARKDOWN
entries = Pressa::Drafts::Repo.new(dir:).read_entries
assert_equal(["Newer Draft", "Older Draft"], entries.map(&:title))
end
end
def test_read_entries_skips_files_without_front_matter
Dir.mktmpdir do |dir|
File.write(File.join(dir, "no-front-matter.md"), "# Just a heading\n")
entries = Pressa::Drafts::Repo.new(dir:).read_entries
assert_empty(entries)
end
end
end

View file

@ -0,0 +1,38 @@
require "test_helper"
require "tmpdir"
class Pressa::Drafts::WriterTest < Minitest::Test
def site
@site ||= Pressa::Site.new(
author: "Sami Samhuri",
email: "sami@samhuri.net",
title: "samhuri.net",
description: "blog",
url: "https://samhuri.net"
)
end
def entries
@entries ||= [
Pressa::Drafts::Entry.new(
slug: "powder-day",
title: "Powder Day at Baker",
timestamp: DateTime.parse("2025-11-05T10:00:00-08:00"),
path: "/drafts/powder-day/"
)
]
end
def test_write_index_writes_drafts_index_page
Dir.mktmpdir do |dir|
Pressa::Drafts::Writer.new(site:, entries:).write_index(target_path: dir)
index_path = File.join(dir, "drafts", "index.html")
assert(File.exist?(index_path))
html = File.read(index_path)
assert_includes(html, "Drafts")
assert_includes(html, "Powder Day at Baker")
assert_includes(html, "/drafts/powder-day/")
end
end
end

View file

@ -176,6 +176,64 @@ class Pressa::SiteGeneratorRenderingTest < Minitest::Test
end
end
def test_generate_writes_drafts_index_for_html_output
Dir.mktmpdir do |root|
source_path = File.join(root, "source")
target_path = File.join(root, "target")
drafts_dir = File.join(source_path, "public", "drafts")
FileUtils.mkdir_p(drafts_dir)
File.write(File.join(drafts_dir, "powder-day.md"), <<~MARKDOWN)
---
Author: Shaun White
Title: Powder Day at Baker
Date: unpublished
Timestamp: 2025-11-05T10:00:00-08:00
---
TKTK
MARKDOWN
plugin = PluginSpy.new
renderer = MarkdownRendererSpy.new
site = build_site(plugin:, renderer:)
Pressa::SiteGenerator.new(site:).generate(source_path:, target_path:)
index_path = File.join(target_path, "drafts", "index.html")
assert(File.exist?(index_path))
assert_includes(File.read(index_path), "Powder Day at Baker")
end
end
def test_generate_skips_drafts_index_for_gemini_output
Dir.mktmpdir do |root|
source_path = File.join(root, "source")
target_path = File.join(root, "target")
drafts_dir = File.join(source_path, "public", "drafts")
FileUtils.mkdir_p(drafts_dir)
File.write(File.join(drafts_dir, "powder-day.md"), <<~MARKDOWN)
---
Author: Shaun White
Title: Powder Day at Baker
Date: unpublished
Timestamp: 2025-11-05T10:00:00-08:00
---
TKTK
MARKDOWN
plugin = PluginSpy.new
renderer = MarkdownRendererSpy.new
site = build_gemini_site(plugin:, renderer:)
Pressa::SiteGenerator.new(site:).generate(source_path:, target_path:)
refute(File.exist?(File.join(target_path, "drafts", "index.html")))
end
end
def test_generate_skips_tweets_directory_for_gemini_output
Dir.mktmpdir do |root|
source_path = File.join(root, "source")