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:
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [development]
|
## [development]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Fediverse attribution:** a `fediverse_creator` frontmatter field (e.g.
|
||||||
|
`@user@mastodon.social`) plus a site-wide `[site].fediverse_creator` default
|
||||||
|
in `config.toml`. The value is exposed as `fediverse_creator` on post and
|
||||||
|
site payloads, rendered as `<dc:creator>` in the RSS feed, as `authors[].name`
|
||||||
|
in the JSON feed, and can be consumed by a front-end to emit
|
||||||
|
`<meta name="fediverse:creator">` on rendered pages. Per-post values
|
||||||
|
override the site default.
|
||||||
|
|
||||||
## [0.2.0] — 2026-06-25
|
## [0.2.0] — 2026-06-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ slug = "my-post" # optional, defaults to the filename
|
|||||||
date = 2026-01-15 # first-class TOML date
|
date = 2026-01-15 # first-class TOML date
|
||||||
lang = "en" # language code
|
lang = "en" # language code
|
||||||
author = "Petr Balvín" # optional
|
author = "Petr Balvín" # optional
|
||||||
|
fediverse_creator = "@petrbalvin@mastodon.social" # optional, for Mastodon attribution
|
||||||
tags = ["ruby", "web"] # optional
|
tags = ["ruby", "web"] # optional
|
||||||
draft = false # optional
|
draft = false # optional
|
||||||
excerpt = "Short summary" # optional, auto-derived from body if missing
|
excerpt = "Short summary" # optional, auto-derived from body if missing
|
||||||
@@ -113,6 +114,21 @@ front-end can offer a language switcher.
|
|||||||
Set `all_langs = true` on a post to show it in **every** language (useful for
|
Set `all_langs = true` on a post to show it in **every** language (useful for
|
||||||
posts that have no per-language translations).
|
posts that have no per-language translations).
|
||||||
|
|
||||||
|
### Fediverse attribution
|
||||||
|
|
||||||
|
Set `fediverse_creator = "@user@instance.tld"` on a post to attach a Mastodon
|
||||||
|
handle to it. The value is exposed as `fediverse_creator` on the post payload
|
||||||
|
(`/api/volumen/posts/{slug}` and `/api/volumen/posts`), and as `<dc:creator>` in
|
||||||
|
the RSS feed and `authors[].name` in the JSON feed. A front-end consuming the
|
||||||
|
API can drop it straight into `<head>`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta name="fediverse:creator" content="@petrbalvin@mastodon.social">
|
||||||
|
```
|
||||||
|
|
||||||
|
A site-wide default can be set in `config.toml` (`[site].fediverse_creator`); a
|
||||||
|
per-post value takes precedence.
|
||||||
|
|
||||||
### Why TOML instead of YAML
|
### Why TOML instead of YAML
|
||||||
|
|
||||||
- **First-class dates** — `date = 2026-01-15` is a real date, not a guess.
|
- **First-class dates** — `date = 2026-01-15` is a real date, not a guess.
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ Returns site metadata from the configuration.
|
|||||||
"description": "A blog powered by volumen.",
|
"description": "A blog powered by volumen.",
|
||||||
"base_url": "https://example.com",
|
"base_url": "https://example.com",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"author": "Anonymous"
|
"author": "Anonymous",
|
||||||
|
"fediverse_creator": "@petrbalvin@mastodon.social"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`fediverse_creator` is `null` when the site-wide default is not set; a per-post
|
||||||
|
value (see below) takes precedence when present.
|
||||||
|
|
||||||
## `GET /api/volumen/posts`
|
## `GET /api/volumen/posts`
|
||||||
|
|
||||||
Paginated list of published posts (drafts are excluded), newest first.
|
Paginated list of published posts (drafts are excluded), newest first.
|
||||||
@@ -46,6 +50,7 @@ Posts with `all_langs = true` in their frontmatter match every `lang` filter.
|
|||||||
"lang": "en",
|
"lang": "en",
|
||||||
"tags": ["intro"],
|
"tags": ["intro"],
|
||||||
"author": null,
|
"author": null,
|
||||||
|
"fediverse_creator": null,
|
||||||
"cover": null,
|
"cover": null,
|
||||||
"reading_time": 1,
|
"reading_time": 1,
|
||||||
"translations": {},
|
"translations": {},
|
||||||
@@ -70,6 +75,7 @@ no match.
|
|||||||
"lang": "en",
|
"lang": "en",
|
||||||
"tags": ["intro"],
|
"tags": ["intro"],
|
||||||
"author": null,
|
"author": null,
|
||||||
|
"fediverse_creator": "@petrbalvin@mastodon.social",
|
||||||
"cover": null,
|
"cover": null,
|
||||||
"reading_time": 1,
|
"reading_time": 1,
|
||||||
"translations": {},
|
"translations": {},
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ description = "A blog powered by volumen."
|
|||||||
base_url = "https://example.com"
|
base_url = "https://example.com"
|
||||||
language = "en"
|
language = "en"
|
||||||
author = "Anonymous"
|
author = "Anonymous"
|
||||||
|
# Site-wide fediverse handle (e.g. Mastodon). Exposed as `fediverse_creator`
|
||||||
|
# on /api/volumen/site and used as the fallback in the RSS / JSON feeds when
|
||||||
|
# a post does not set its own `fediverse_creator` frontmatter field. Leave
|
||||||
|
# empty if you do not publish to the fediverse.
|
||||||
|
fediverse_creator = "@you@example.social"
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
# scrypt hash produced by `volumen hash-password`.
|
# scrypt hash produced by `volumen hash-password`.
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ module Volumen
|
|||||||
"description" => "A blog powered by volumen.",
|
"description" => "A blog powered by volumen.",
|
||||||
"base_url" => "https://example.com",
|
"base_url" => "https://example.com",
|
||||||
"language" => "en",
|
"language" => "en",
|
||||||
"author" => "Anonymous"
|
"author" => "Anonymous",
|
||||||
|
"fediverse_creator" => nil
|
||||||
}.freeze,
|
}.freeze,
|
||||||
"admin" => {
|
"admin" => {
|
||||||
"password_hash" => "",
|
"password_hash" => "",
|
||||||
@@ -46,6 +47,9 @@ module Volumen
|
|||||||
base_url = "https://example.com"
|
base_url = "https://example.com"
|
||||||
language = "en"
|
language = "en"
|
||||||
author = "Anonymous"
|
author = "Anonymous"
|
||||||
|
# Default fediverse handle surfaced in <meta name="fediverse:creator">
|
||||||
|
# by frontends consuming the API. Leave empty to disable.
|
||||||
|
fediverse_creator = ""
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
# Bootstrap admin login: paste a hash from `volumen hash-password` to sign
|
# Bootstrap admin login: paste a hash from `volumen hash-password` to sign
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ require_relative "markdown"
|
|||||||
module Volumen
|
module Volumen
|
||||||
# A single blog post: TOML frontmatter metadata plus a Markdown body.
|
# A single blog post: TOML frontmatter metadata plus a Markdown body.
|
||||||
class Post
|
class Post
|
||||||
|
FEDIVERSE_CREATOR_REGEX = /\A@[^@\s]+@[^@\s]+\z/
|
||||||
|
|
||||||
attr_reader :metadata, :body
|
attr_reader :metadata, :body
|
||||||
attr_accessor :path
|
attr_accessor :path
|
||||||
|
|
||||||
@@ -56,6 +58,18 @@ module Volumen
|
|||||||
metadata["cover"]
|
metadata["cover"]
|
||||||
end
|
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
|
def publish_at
|
||||||
metadata["publish_at"]
|
metadata["publish_at"]
|
||||||
end
|
end
|
||||||
@@ -112,6 +126,7 @@ module Volumen
|
|||||||
"lang" => lang,
|
"lang" => lang,
|
||||||
"tags" => tags,
|
"tags" => tags,
|
||||||
"author" => author,
|
"author" => author,
|
||||||
|
"fediverse_creator" => fediverse_creator,
|
||||||
"cover" => cover,
|
"cover" => cover,
|
||||||
"reading_time" => reading_time,
|
"reading_time" => reading_time,
|
||||||
"translations" => translations,
|
"translations" => translations,
|
||||||
|
|||||||
+34
-5
@@ -84,7 +84,12 @@ module Volumen
|
|||||||
end
|
end
|
||||||
|
|
||||||
def site_payload
|
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
|
end
|
||||||
|
|
||||||
def posts_payload
|
def posts_payload
|
||||||
@@ -146,7 +151,7 @@ module Volumen
|
|||||||
items = published_posts.first(20).map { |post| feed_item(post) }.join("\n")
|
items = published_posts.first(20).map { |post| feed_item(post) }.join("\n")
|
||||||
<<~XML
|
<<~XML
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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>
|
<channel>
|
||||||
<title>#{escape(site["title"])}</title>
|
<title>#{escape(site["title"])}</title>
|
||||||
<link>#{escape(base_url)}</link>
|
<link>#{escape(base_url)}</link>
|
||||||
@@ -165,12 +170,14 @@ module Volumen
|
|||||||
else
|
else
|
||||||
""
|
""
|
||||||
end
|
end
|
||||||
|
creator = post.fediverse_creator
|
||||||
|
creator_tag = creator ? "\n <dc:creator>#{escape(creator)}</dc:creator>" : ""
|
||||||
<<~XML.chomp
|
<<~XML.chomp
|
||||||
<item>
|
<item>
|
||||||
<title>#{escape(post.title)}</title>
|
<title>#{escape(post.title)}</title>
|
||||||
<link>#{escape(link)}</link>
|
<link>#{escape(link)}</link>
|
||||||
<guid>#{escape(link)}</guid>
|
<guid>#{escape(link)}</guid>
|
||||||
<description>#{escape(post.excerpt)}</description>#{pub}
|
<description>#{escape(post.excerpt)}</description>#{pub}#{creator_tag}
|
||||||
</item>
|
</item>
|
||||||
XML
|
XML
|
||||||
end
|
end
|
||||||
@@ -208,6 +215,8 @@ module Volumen
|
|||||||
"date_published" => iso_date(post.metadata["date"]),
|
"date_published" => iso_date(post.metadata["date"]),
|
||||||
"tags" => post.tags
|
"tags" => post.tags
|
||||||
}
|
}
|
||||||
|
author = post.fediverse_creator || site_fediverse_creator
|
||||||
|
item["authors"] = [{ "name" => author }] if author
|
||||||
item.reject { |_k, v| v.nil? || v == "" || v == [] }
|
item.reject { |_k, v| v.nil? || v == "" || v == [] }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -401,7 +410,15 @@ module Volumen
|
|||||||
existing = store.find(params["slug"])
|
existing = store.find(params["slug"])
|
||||||
halt(404, "Not found") if existing.nil?
|
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/")
|
redirect to("/admin/")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -410,7 +427,7 @@ module Volumen
|
|||||||
return "Invalid slug." unless post.slug.match?(SLUG_REGEX)
|
return "Invalid slug." unless post.slug.match?(SLUG_REGEX)
|
||||||
return "A post with that slug already exists." if store.find(post.slug)
|
return "A post with that slug already exists." if store.find(post.slug)
|
||||||
|
|
||||||
nil
|
fediverse_creator_error(post)
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_from_params(http_params, existing: nil)
|
def post_from_params(http_params, existing: nil)
|
||||||
@@ -419,6 +436,7 @@ module Volumen
|
|||||||
"slug" => presence(http_params["slug"]) || existing&.slug,
|
"slug" => presence(http_params["slug"]) || existing&.slug,
|
||||||
"lang" => presence(http_params["lang"]),
|
"lang" => presence(http_params["lang"]),
|
||||||
"author" => presence(http_params["author"]),
|
"author" => presence(http_params["author"]),
|
||||||
|
"fediverse_creator" => presence(http_params["fediverse_creator"]),
|
||||||
"date" => parse_date(http_params["date"]),
|
"date" => parse_date(http_params["date"]),
|
||||||
"publish_at" => parse_date(http_params["publish_at"]),
|
"publish_at" => parse_date(http_params["publish_at"]),
|
||||||
"tags" => parse_tags(http_params["tags"]),
|
"tags" => parse_tags(http_params["tags"]),
|
||||||
@@ -432,6 +450,17 @@ module Volumen
|
|||||||
post
|
post
|
||||||
end
|
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)
|
def clean_metadata(metadata)
|
||||||
metadata.reject { |_key, value| value.nil? || value == "" || value == [] || value == false }
|
metadata.reject { |_key, value| value.nil? || value == "" || value == [] || value == false }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>Language <input type="text" name="lang" value="<%= h(@post.lang) %>"></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>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>Date <input type="date" name="date" value="<%= h(@post.date_string) %>"></label>
|
||||||
<label>Publish at
|
<label>Publish at
|
||||||
<input type="date" name="publish_at"
|
<input type="date" name="publish_at"
|
||||||
|
|||||||
@@ -77,4 +77,61 @@ class PostTest < Minitest::Test
|
|||||||
post = Volumen::Post.new(metadata: {})
|
post = Volumen::Post.new(metadata: {})
|
||||||
refute_predicate post, :scheduled?
|
refute_predicate post, :scheduled?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_fediverse_creator_returns_metadata_value
|
||||||
|
post = Volumen::Post.new(
|
||||||
|
metadata: { "slug" => "x", "fediverse_creator" => "@user@example.social" }
|
||||||
|
)
|
||||||
|
assert_equal "@user@example.social", post.fediverse_creator
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fediverse_creator_is_nil_when_missing
|
||||||
|
post = Volumen::Post.new(metadata: { "slug" => "x" })
|
||||||
|
assert_nil post.fediverse_creator
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fediverse_creator_is_nil_when_blank
|
||||||
|
post = Volumen::Post.new(metadata: { "slug" => "x", "fediverse_creator" => " " })
|
||||||
|
assert_nil post.fediverse_creator
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fediverse_creator_validation_accepts_well_formed_handles
|
||||||
|
post = Volumen::Post.new(
|
||||||
|
metadata: { "slug" => "x", "fediverse_creator" => "@ada@lovelace.org" }
|
||||||
|
)
|
||||||
|
assert_predicate post, :valid_fediverse_creator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fediverse_creator_validation_rejects_malformed_handles
|
||||||
|
[
|
||||||
|
"ada@example.com",
|
||||||
|
"@only-one-at",
|
||||||
|
"@user@example.com extra",
|
||||||
|
"@user@",
|
||||||
|
"@@example.com"
|
||||||
|
].each do |value|
|
||||||
|
post = Volumen::Post.new(metadata: { "slug" => "x", "fediverse_creator" => value })
|
||||||
|
refute_predicate post, :valid_fediverse_creator?, "expected #{value.inspect} to be invalid"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_fediverse_creator_validation_accepts_padded_value
|
||||||
|
post = Volumen::Post.new(
|
||||||
|
metadata: { "slug" => "x", "fediverse_creator" => " @user@example.com " }
|
||||||
|
)
|
||||||
|
assert_predicate post, :valid_fediverse_creator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_summary_includes_fediverse_creator
|
||||||
|
post = Volumen::Post.new(
|
||||||
|
metadata: { "slug" => "x", "fediverse_creator" => "@user@example.social" },
|
||||||
|
body: "body"
|
||||||
|
)
|
||||||
|
assert_equal "@user@example.social", post.summary["fediverse_creator"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_summary_omits_fediverse_creator_when_absent
|
||||||
|
post = Volumen::Post.new(metadata: { "slug" => "x" }, body: "body")
|
||||||
|
assert_nil post.summary["fediverse_creator"]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+97
-1
@@ -98,7 +98,7 @@ class ServerTest < Minitest::Test
|
|||||||
assert_predicate last_response, :ok?
|
assert_predicate last_response, :ok?
|
||||||
assert_equal "application/rss+xml", last_response["Content-Type"]&.split(";")&.first
|
assert_equal "application/rss+xml", last_response["Content-Type"]&.split(";")&.first
|
||||||
body = last_response.body
|
body = last_response.body
|
||||||
assert_includes body, "<rss version=\"2.0\">"
|
assert_includes body, "<rss version=\"2.0\""
|
||||||
assert_includes body, "<title>Hello</title>"
|
assert_includes body, "<title>Hello</title>"
|
||||||
refute_includes body, "Draft"
|
refute_includes body, "Draft"
|
||||||
assert_includes body, "<language>en</language>"
|
assert_includes body, "<language>en</language>"
|
||||||
@@ -154,6 +154,102 @@ class ServerTest < Minitest::Test
|
|||||||
refute_includes data["items"].map { |i| i["title"] }, "Draft"
|
refute_includes data["items"].map { |i| i["title"] }, "Draft"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_post_summary_exposes_fediverse_creator
|
||||||
|
File.write(File.join(@dir, "attributed.md"), <<~POST)
|
||||||
|
+++
|
||||||
|
title = "Attributed"
|
||||||
|
slug = "attributed"
|
||||||
|
fediverse_creator = "@ada@lovelace.org"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Body.
|
||||||
|
POST
|
||||||
|
get "/api/volumen/posts/attributed"
|
||||||
|
assert_predicate last_response, :ok?
|
||||||
|
data = JSON.parse(last_response.body)
|
||||||
|
assert_equal "@ada@lovelace.org", data["fediverse_creator"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_post_summary_fediverse_creator_is_null_when_not_set
|
||||||
|
get "/api/volumen/posts/hello"
|
||||||
|
assert_predicate last_response, :ok?
|
||||||
|
assert_nil JSON.parse(last_response.body)["fediverse_creator"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_posts_list_summary_exposes_fediverse_creator
|
||||||
|
File.write(File.join(@dir, "fed.md"), <<~POST)
|
||||||
|
+++
|
||||||
|
title = "Fed"
|
||||||
|
slug = "fed"
|
||||||
|
fediverse_creator = "@user@example.social"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Body.
|
||||||
|
POST
|
||||||
|
get "/api/volumen/posts"
|
||||||
|
data = JSON.parse(last_response.body)
|
||||||
|
fed = data["posts"].find { |post| post["slug"] == "fed" }
|
||||||
|
assert_equal "@user@example.social", fed["fediverse_creator"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_site_endpoint_exposes_fediverse_creator_default
|
||||||
|
get "/api/volumen/site"
|
||||||
|
assert_predicate last_response, :ok?
|
||||||
|
assert JSON.parse(last_response.body).key?("fediverse_creator")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_rss_feed_includes_dc_creator_when_set
|
||||||
|
File.write(File.join(@dir, "rss.md"), <<~POST)
|
||||||
|
+++
|
||||||
|
title = "Rss"
|
||||||
|
slug = "rss"
|
||||||
|
fediverse_creator = "@rss@example.com"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Body.
|
||||||
|
POST
|
||||||
|
get "/api/volumen/feed.xml"
|
||||||
|
assert_includes last_response.body, "<dc:creator>@rss@example.com</dc:creator>"
|
||||||
|
assert_includes last_response.body, "xmlns:dc=\"http://purl.org/dc/elements/1.1/\""
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_rss_feed_omits_dc_creator_when_not_set
|
||||||
|
get "/api/volumen/feed.xml"
|
||||||
|
refute_includes last_response.body, "<dc:creator>"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_json_feed_includes_authors_from_post
|
||||||
|
File.write(File.join(@dir, "jsonfed.md"), <<~POST)
|
||||||
|
+++
|
||||||
|
title = "Jsonfed"
|
||||||
|
slug = "jsonfed"
|
||||||
|
fediverse_creator = "@jf@example.io"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Body.
|
||||||
|
POST
|
||||||
|
get "/api/volumen/feed.json"
|
||||||
|
item = JSON.parse(last_response.body)["items"].find { |i| i["id"] == "https://example.com/jsonfed" }
|
||||||
|
assert_equal [{ "name" => "@jf@example.io" }], item["authors"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_json_feed_falls_back_to_site_default_fediverse_creator
|
||||||
|
Dir.chdir(@dir) do
|
||||||
|
File.write("config.toml", <<~TOML)
|
||||||
|
[site]
|
||||||
|
fediverse_creator = "@owner@example.com"
|
||||||
|
TOML
|
||||||
|
end
|
||||||
|
@config = Volumen::Config.load(
|
||||||
|
path: File.join(@dir, "config.toml"),
|
||||||
|
overrides: { content: @dir }
|
||||||
|
)
|
||||||
|
get "/api/volumen/feed.json"
|
||||||
|
items = JSON.parse(last_response.body)["items"]
|
||||||
|
expected = [{ "name" => "@owner@example.com" }]
|
||||||
|
assert(items.all? { |i| i["authors"] == expected })
|
||||||
|
end
|
||||||
|
|
||||||
def test_search_by_title
|
def test_search_by_title
|
||||||
get "/api/volumen/posts?q=Hello"
|
get "/api/volumen/posts?q=Hello"
|
||||||
data = JSON.parse(last_response.body)
|
data = JSON.parse(last_response.body)
|
||||||
|
|||||||
Reference in New Issue
Block a user