Files
volumen/lib/volumen/post.rb
T
petrbalvin 73be893bf8 feat: add scheduled publishing with publish_at frontmatter field
Posts with a future publish_at date are hidden from the public API
and feeds, same as drafts. The admin form exposes a Publish at field.
2026-06-25 22:06:49 +02:00

138 lines
2.7 KiB
Ruby

# frozen_string_literal: true
require "date"
require_relative "frontmatter"
require_relative "markdown"
module Volumen
# A single blog post: TOML frontmatter metadata plus a Markdown body.
class Post
attr_reader :metadata, :body
attr_accessor :path
def initialize(metadata: {}, body: "")
@metadata = (metadata || {}).transform_keys(&:to_s)
@body = body.to_s
end
def self.parse(content)
metadata, body = Frontmatter.parse(content)
new(metadata: metadata, body: body)
end
def slug
metadata["slug"]
end
def title
metadata["title"]
end
def lang
metadata["lang"]
end
def author
metadata["author"]
end
def tags
Array(metadata["tags"]).map(&:to_s)
end
def draft?
metadata["draft"] == true
end
def all_langs?
metadata["all_langs"] == true
end
def translations
metadata["translations"] || {}
end
def cover
metadata["cover"]
end
def publish_at
metadata["publish_at"]
end
def scheduled?
value = publish_at
return false if value.nil?
date = case value
when Date then value
when Time then value.to_date
else Date.iso8601(value.to_s)
end
date > Date.today
rescue ArgumentError
false
end
def reading_time
words = body.split(/\s+/).count { |word| !word.empty? }
[(words / 200.0).ceil, 1].max
end
def date_string
value = metadata["date"]
case value
when nil then nil
when Date, Time then value.strftime("%Y-%m-%d")
else value.to_s
end
end
def excerpt
text = metadata["excerpt"]
return text if text && !text.to_s.empty?
derived_excerpt
end
def html
Markdown.render(body)
end
def to_file
Frontmatter.dump(metadata, body)
end
def summary
{
"slug" => slug,
"title" => title,
"excerpt" => excerpt,
"date" => date_string,
"lang" => lang,
"tags" => tags,
"author" => author,
"cover" => cover,
"reading_time" => reading_time,
"translations" => translations,
"url" => "/api/volumen/posts/#{slug}"
}
end
def detail
summary.merge("body" => body, "html" => html)
end
private
def derived_excerpt(limit = 200)
paragraph = body.split(/\n\s*\n/).map(&:strip)
.find { |para| !para.empty? && !para.start_with?("#") }
return "" if paragraph.nil?
stripped = paragraph.gsub(/<[^>]+>/, "").gsub(/[*_`>#]/, "").strip
stripped.length > limit ? "#{stripped[0, limit].rstrip}…" : stripped
end
end
end