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(
format_config["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(
@ -236,7 +238,9 @@ module Pressa
home_links = build_output_links(
format_config["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(
format_config["recent_posts_limit"],
@ -339,11 +343,21 @@ module Pressa
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.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)
raise ValidationError, "Expected #{context}[#{index}] to be a table"
raise ValidationError, "Expected #{context}[#{index}] to be a String or table"
end
allowed_keys = allow_icon ? %w[label href icon] : %w[label href]
@ -353,16 +367,23 @@ module Pressa
context: "#{context}[#{index}]"
)
label = entry["label"]
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?
raise ValidationError, "Expected #{context}[#{index}].href to be a non-empty String"
end
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"]
unless allow_icon
if entry.key?("icon")
@ -376,7 +397,7 @@ module Pressa
raise ValidationError, "Expected #{context}[#{index}].icon to be a non-empty String"
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

View file

@ -20,7 +20,12 @@ module Pressa
def write_recent_posts(target_path:, limit: RECENT_POSTS_LIMIT)
rows = ["# #{@site.title}", ""]
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
rows << "" unless home_links.empty?
rows << "## Recent posts"

View file

@ -6,7 +6,8 @@ module Pressa
end
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 :icon, Types::String.optional.default(nil)
end

View file

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

View file

@ -29,14 +29,14 @@ remote_links = [
[outputs.gemini]
recent_posts_limit = 20
home_links = [
{"label": "About", "href": "/about"},
{"label": "Posts", "href": "/posts"},
{"label": "Projects", "href": "/projects"},
{"label": "Mastodon", "href": "https://techhub.social/@sjs"},
{"label": "GitHub", "href": "https://github.com/samsonjs"},
{"label": "RSS (Gemini)", "href": "/posts/feed.gmi"},
{"label": "RSS (Web)", "href": "https://samhuri.net/feed.xml"},
{"label": "Email", "href": "mailto:sami@samhuri.net"}
"/about",
"/posts",
"/projects",
"https://techhub.social/@sjs",
"https://github.com/samsonjs",
"/posts/feed.gmi",
"https://samhuri.net/feed.xml",
"mailto:sami@samhuri.net"
]
exclude_public = [
"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(["tweets/**"], site.public_excludes)
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
@ -561,6 +561,35 @@ class Pressa::Config::LoaderTest < Minitest::Test
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
def with_temp_config
@ -588,10 +617,7 @@ class Pressa::Config::LoaderTest < Minitest::Test
[outputs.gemini]
recent_posts_limit = 20
home_links = [
{"label": "About", "href": "/about/"},
{"label": "GitHub", "href": "https://github.com/samsonjs"}
]
home_links = ["/about/", "https://github.com/samsonjs"]
exclude_public = ["tweets/**"]
TOML