Fix detecting gemini:// links in JS and remove unused properties from site.toml

This commit is contained in:
Sami Samhuri 2026-02-14 16:59:16 -08:00
parent 5255247823
commit d2fc41fb1e
No known key found for this signature in database
6 changed files with 122 additions and 65 deletions

View file

@ -214,7 +214,9 @@ module Pressa
remote_links = build_output_links( remote_links = build_output_links(
format_config["remote_links"], format_config["remote_links"],
context: "site.toml outputs.html.remote_links", context: "site.toml outputs.html.remote_links",
allow_icon: true allow_icon: true,
allow_label_optional: false,
allow_string_entries: false
) )
HTMLOutputOptions.new( HTMLOutputOptions.new(
@ -236,7 +238,9 @@ module Pressa
home_links = build_output_links( home_links = build_output_links(
format_config["home_links"], format_config["home_links"],
context: "site.toml outputs.gemini.home_links", context: "site.toml outputs.gemini.home_links",
allow_icon: false allow_icon: false,
allow_label_optional: true,
allow_string_entries: true
) )
recent_posts_limit = build_recent_posts_limit( recent_posts_limit = build_recent_posts_limit(
format_config["recent_posts_limit"], format_config["recent_posts_limit"],
@ -339,11 +343,21 @@ module Pressa
end end
end end
def build_output_links(value, context:, allow_icon:) def build_output_links(value, context:, allow_icon:, allow_label_optional:, allow_string_entries:)
entries = array_or_empty(value, context) entries = array_or_empty(value, context)
entries.map.with_index do |entry, index| entries.map.with_index do |entry, index|
if allow_string_entries && entry.is_a?(String)
href = entry
unless !href.strip.empty?
raise ValidationError, "Expected #{context}[#{index}] to be a non-empty String"
end
validate_link_href!(href.strip, context: "#{context}[#{index}]")
next OutputLink.new(label: nil, href: href.strip, icon: nil)
end
unless entry.is_a?(Hash) unless entry.is_a?(Hash)
raise ValidationError, "Expected #{context}[#{index}] to be a table" raise ValidationError, "Expected #{context}[#{index}] to be a String or table"
end end
allowed_keys = allow_icon ? %w[label href icon] : %w[label href] allowed_keys = allow_icon ? %w[label href icon] : %w[label href]
@ -353,16 +367,23 @@ module Pressa
context: "#{context}[#{index}]" context: "#{context}[#{index}]"
) )
label = entry["label"]
href = entry["href"] href = entry["href"]
unless label.is_a?(String) && !label.strip.empty?
raise ValidationError, "Expected #{context}[#{index}].label to be a non-empty String"
end
unless href.is_a?(String) && !href.strip.empty? unless href.is_a?(String) && !href.strip.empty?
raise ValidationError, "Expected #{context}[#{index}].href to be a non-empty String" raise ValidationError, "Expected #{context}[#{index}].href to be a non-empty String"
end end
validate_link_href!(href.strip, context: "#{context}[#{index}].href") validate_link_href!(href.strip, context: "#{context}[#{index}].href")
label = entry["label"]
if label.nil?
unless allow_label_optional
raise ValidationError, "Expected #{context}[#{index}].label to be a non-empty String"
end
else
unless label.is_a?(String) && !label.strip.empty?
raise ValidationError, "Expected #{context}[#{index}].label to be a non-empty String"
end
end
icon = entry["icon"] icon = entry["icon"]
unless allow_icon unless allow_icon
if entry.key?("icon") if entry.key?("icon")
@ -376,7 +397,7 @@ module Pressa
raise ValidationError, "Expected #{context}[#{index}].icon to be a non-empty String" raise ValidationError, "Expected #{context}[#{index}].icon to be a non-empty String"
end end
OutputLink.new(label: label.strip, href: href.strip, icon: icon&.strip) OutputLink.new(label: label&.strip, href: href.strip, icon: icon&.strip)
end end
end end

View file

@ -20,7 +20,12 @@ module Pressa
def write_recent_posts(target_path:, limit: RECENT_POSTS_LIMIT) def write_recent_posts(target_path:, limit: RECENT_POSTS_LIMIT)
rows = ["# #{@site.title}", ""] rows = ["# #{@site.title}", ""]
home_links.each do |link| home_links.each do |link|
rows << "=> #{link.href}" label = link.label&.strip
rows << if label.nil? || label.empty?
"=> #{link.href}"
else
"=> #{link.href} #{label}"
end
end end
rows << "" unless home_links.empty? rows << "" unless home_links.empty?
rows << "## Recent posts" rows << "## Recent posts"

View file

@ -6,7 +6,8 @@ module Pressa
end end
class OutputLink < Dry::Struct class OutputLink < Dry::Struct
attribute :label, Types::String # label is required for HTML remote links, but Gemini home_links may omit it.
attribute :label, Types::String.optional.default(nil)
attribute :href, Types::String attribute :href, Types::String
attribute :icon, Types::String.optional.default(nil) attribute :icon, Types::String.optional.default(nil)
end end

View file

@ -199,60 +199,64 @@ module Pressa
} }
function setupGeminiFallback() { function setupGeminiFallback() {
var link = document.querySelector( var links = document.querySelectorAll(
'header.primary nav.remote li.gemini a[href^="gemini://"]' 'header.primary nav.remote a[href^="gemini://"]'
); );
if (!link) return; if (!links || links.length === 0) return;
link.addEventListener("click", function (e) { for (var i = 0; i < links.length; i++) {
if (!isPlainLeftClick(e)) return; (function (link) {
link.addEventListener("click", function (e) {
if (!isPlainLeftClick(e)) return;
e.preventDefault(); e.preventDefault();
var geminiHref = link.getAttribute("href"); var geminiHref = link.getAttribute("href");
var fallbackHref = "https://geminiprotocol.net"; var fallbackHref = "https://geminiprotocol.net";
var done = false; var done = false;
var fallbackTimer = null; var fallbackTimer = null;
function cleanup() { function cleanup() {
if (fallbackTimer) window.clearTimeout(fallbackTimer); if (fallbackTimer) window.clearTimeout(fallbackTimer);
document.removeEventListener("visibilitychange", onVisibilityChange); document.removeEventListener("visibilitychange", onVisibilityChange);
window.removeEventListener("pagehide", onPageHide); window.removeEventListener("pagehide", onPageHide);
window.removeEventListener("blur", onBlur); window.removeEventListener("blur", onBlur);
} }
function markDone() { function markDone() {
done = true; done = true;
cleanup(); cleanup();
} }
function onVisibilityChange() { function onVisibilityChange() {
// If a handler opens and the browser backgrounded, consider it "successful". // If a handler opens and the browser backgrounded, consider it "successful".
if (document.visibilityState === "hidden") markDone(); if (document.visibilityState === "hidden") markDone();
} }
function onPageHide() { function onPageHide() {
markDone(); markDone();
} }
function onBlur() { function onBlur() {
// Some browsers blur the page when a protocol handler is invoked. // Some browsers blur the page when a protocol handler is invoked.
markDone(); markDone();
} }
document.addEventListener("visibilitychange", onVisibilityChange); document.addEventListener("visibilitychange", onVisibilityChange);
window.addEventListener("pagehide", onPageHide, { once: true }); window.addEventListener("pagehide", onPageHide, { once: true });
window.addEventListener("blur", onBlur, { once: true }); window.addEventListener("blur", onBlur, { once: true });
// If we're still here shortly after attempting navigation, assume it failed. // If we're still here shortly after attempting navigation, assume it failed.
fallbackTimer = window.setTimeout(function () { fallbackTimer = window.setTimeout(function () {
if (done) return; if (done) return;
window.location.href = fallbackHref; window.location.href = fallbackHref;
}, 900); }, 900);
window.location.href = geminiHref; window.location.href = geminiHref;
}); });
})(links[i]);
}
} }
if (document.readyState === "loading") { if (document.readyState === "loading") {

View file

@ -29,14 +29,14 @@ remote_links = [
[outputs.gemini] [outputs.gemini]
recent_posts_limit = 20 recent_posts_limit = 20
home_links = [ home_links = [
{"label": "About", "href": "/about"}, "/about",
{"label": "Posts", "href": "/posts"}, "/posts",
{"label": "Projects", "href": "/projects"}, "/projects",
{"label": "Mastodon", "href": "https://techhub.social/@sjs"}, "https://techhub.social/@sjs",
{"label": "GitHub", "href": "https://github.com/samsonjs"}, "https://github.com/samsonjs",
{"label": "RSS (Gemini)", "href": "/posts/feed.gmi"}, "/posts/feed.gmi",
{"label": "RSS (Web)", "href": "https://samhuri.net/feed.xml"}, "https://samhuri.net/feed.xml",
{"label": "Email", "href": "mailto:sami@samhuri.net"} "mailto:sami@samhuri.net"
] ]
exclude_public = [ exclude_public = [
"tweets/**", "tweets/**",

View file

@ -41,7 +41,7 @@ class Pressa::Config::LoaderTest < Minitest::Test
assert_equal(["Pressa::Utils::GeminiMarkdownRenderer"], site.renderers.map(&:class).map(&:name)) assert_equal(["Pressa::Utils::GeminiMarkdownRenderer"], site.renderers.map(&:class).map(&:name))
assert_equal(["tweets/**"], site.public_excludes) assert_equal(["tweets/**"], site.public_excludes)
assert_equal(20, site.gemini_output_options&.recent_posts_limit) assert_equal(20, site.gemini_output_options&.recent_posts_limit)
assert_equal(["About", "GitHub"], site.gemini_output_options&.home_links&.map(&:label)) assert_equal(["/about/", "https://github.com/samsonjs"], site.gemini_output_options&.home_links&.map(&:href))
end end
end end
@ -561,6 +561,35 @@ class Pressa::Config::LoaderTest < Minitest::Test
end end
end end
def test_build_site_allows_string_home_links_and_optional_labels_for_gemini
Dir.mktmpdir do |dir|
File.write(File.join(dir, "site.toml"), <<~TOML)
author = "Sami Samhuri"
email = "sami@samhuri.net"
title = "samhuri.net"
description = "blog"
url = "https://samhuri.net"
[outputs.gemini]
home_links = [
"/about/",
{"href": "/posts/"},
{"label": "GitHub", "href": "https://github.com/samsonjs"}
]
TOML
File.write(File.join(dir, "projects.toml"), "projects = []\n")
loader = Pressa::Config::Loader.new(source_path: dir)
site = loader.build_site(output_format: "gemini")
assert_equal("gemini", site.output_format)
assert_equal(3, site.gemini_output_options&.home_links&.length)
assert_nil(site.gemini_output_options&.home_links&.at(0)&.label)
assert_nil(site.gemini_output_options&.home_links&.at(1)&.label)
assert_equal("GitHub", site.gemini_output_options&.home_links&.at(2)&.label)
end
end
private private
def with_temp_config def with_temp_config
@ -588,10 +617,7 @@ class Pressa::Config::LoaderTest < Minitest::Test
[outputs.gemini] [outputs.gemini]
recent_posts_limit = 20 recent_posts_limit = 20
home_links = [ home_links = ["/about/", "https://github.com/samsonjs"]
{"label": "About", "href": "/about/"},
{"label": "GitHub", "href": "https://github.com/samsonjs"}
]
exclude_public = ["tweets/**"] exclude_public = ["tweets/**"]
TOML TOML