feat: add fediverse_creator handle for Mastodon attribution

Expose a per-post fediverse_creator frontmatter field plus a site-wide
default in config.toml ([site].fediverse_creator). The handle is returned
in the post and site payloads, rendered as <dc:creator> in the RSS feed
and as authors[].name in the JSON feed, so a front-end can drop it into
<meta name="fediverse:creator"> on rendered pages. Per-post value
overrides the site default. Validated against a simple @user@host regex.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-06-26 17:40:48 +02:00
co-authored by Qwen-Coder
parent 1440e9a1fd
commit 5437cef1cd
10 changed files with 250 additions and 8 deletions
+5 -1
View File
@@ -18,7 +18,8 @@ module Volumen
"description" => "A blog powered by volumen.",
"base_url" => "https://example.com",
"language" => "en",
"author" => "Anonymous"
"author" => "Anonymous",
"fediverse_creator" => nil
}.freeze,
"admin" => {
"password_hash" => "",
@@ -46,6 +47,9 @@ module Volumen
base_url = "https://example.com"
language = "en"
author = "Anonymous"
# Default fediverse handle surfaced in <meta name="fediverse:creator">
# by frontends consuming the API. Leave empty to disable.
fediverse_creator = ""
[admin]
# Bootstrap admin login: paste a hash from `volumen hash-password` to sign
+15
View File
@@ -7,6 +7,8 @@ require_relative "markdown"
module Volumen
# A single blog post: TOML frontmatter metadata plus a Markdown body.
class Post
FEDIVERSE_CREATOR_REGEX = /\A@[^@\s]+@[^@\s]+\z/
attr_reader :metadata, :body
attr_accessor :path
@@ -56,6 +58,18 @@ module Volumen
metadata["cover"]
end
def fediverse_creator
value = metadata["fediverse_creator"]
return nil if value.nil?
text = value.to_s.strip
text.empty? ? nil : text
end
def valid_fediverse_creator?
fediverse_creator&.match?(FEDIVERSE_CREATOR_REGEX)
end
def publish_at
metadata["publish_at"]
end
@@ -112,6 +126,7 @@ module Volumen
"lang" => lang,
"tags" => tags,
"author" => author,
"fediverse_creator" => fediverse_creator,
"cover" => cover,
"reading_time" => reading_time,
"translations" => translations,
+34 -5
View File
@@ -84,7 +84,12 @@ module Volumen
end
def site_payload
config.site.slice("title", "description", "base_url", "language", "author")
config.site.slice("title", "description", "base_url", "language", "author",
"fediverse_creator")
end
def site_fediverse_creator
config.site["fediverse_creator"]
end
def posts_payload
@@ -146,7 +151,7 @@ module Volumen
items = published_posts.first(20).map { |post| feed_item(post) }.join("\n")
<<~XML
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>#{escape(site["title"])}</title>
<link>#{escape(base_url)}</link>
@@ -165,12 +170,14 @@ module Volumen
else
""
end
creator = post.fediverse_creator
creator_tag = creator ? "\n <dc:creator>#{escape(creator)}</dc:creator>" : ""
<<~XML.chomp
<item>
<title>#{escape(post.title)}</title>
<link>#{escape(link)}</link>
<guid>#{escape(link)}</guid>
<description>#{escape(post.excerpt)}</description>#{pub}
<description>#{escape(post.excerpt)}</description>#{pub}#{creator_tag}
</item>
XML
end
@@ -208,6 +215,8 @@ module Volumen
"date_published" => iso_date(post.metadata["date"]),
"tags" => post.tags
}
author = post.fediverse_creator || site_fediverse_creator
item["authors"] = [{ "name" => author }] if author
item.reject { |_k, v| v.nil? || v == "" || v == [] }
end
@@ -401,7 +410,15 @@ module Volumen
existing = store.find(params["slug"])
halt(404, "Not found") if existing.nil?
store.save(post_from_params(params, existing: existing))
post = post_from_params(params, existing: existing)
error = fediverse_creator_error(post)
if error
@mode = :edit
@post = post
@error = error
return erb(:form)
end
store.save(post)
redirect to("/admin/")
end
@@ -410,7 +427,7 @@ module Volumen
return "Invalid slug." unless post.slug.match?(SLUG_REGEX)
return "A post with that slug already exists." if store.find(post.slug)
nil
fediverse_creator_error(post)
end
def post_from_params(http_params, existing: nil)
@@ -419,6 +436,7 @@ module Volumen
"slug" => presence(http_params["slug"]) || existing&.slug,
"lang" => presence(http_params["lang"]),
"author" => presence(http_params["author"]),
"fediverse_creator" => presence(http_params["fediverse_creator"]),
"date" => parse_date(http_params["date"]),
"publish_at" => parse_date(http_params["publish_at"]),
"tags" => parse_tags(http_params["tags"]),
@@ -432,6 +450,17 @@ module Volumen
post
end
def fediverse_creator_error(post)
value = post.metadata["fediverse_creator"]
return nil if value.nil?
if Post::FEDIVERSE_CREATOR_REGEX.match?(value.to_s)
nil
else
"Fediverse creator must look like @user@host."
end
end
def clean_metadata(metadata)
metadata.reject { |_key, value| value.nil? || value == "" || value == [] || value == false }
end
+4
View File
@@ -16,6 +16,10 @@
</label>
<label>Language <input type="text" name="lang" value="<%= h(@post.lang) %>"></label>
<label>Author <input type="text" name="author" value="<%= h(@post.author) %>"></label>
<label>Fediverse creator
<input type="text" name="fediverse_creator" value="<%= h(@post.fediverse_creator) %>"
placeholder="@user@instance.tld">
</label>
<label>Date <input type="date" name="date" value="<%= h(@post.date_string) %>"></label>
<label>Publish at
<input type="date" name="publish_at"