Derive SEO metadata from existing config (#153)

This commit is contained in:
l5y
2025-09-23 08:20:42 +02:00
committed by GitHub
parent 74b3da6f00
commit 17018aeb19
4 changed files with 137 additions and 8 deletions

View File

@@ -73,6 +73,10 @@ The web app can be configured with environment variables (defaults shown):
* `MAX_NODE_DISTANCE_KM` - hide nodes farther than this distance from the center (default: `137`)
* `MATRIX_ROOM` - matrix room id for a footer link (default: `#meshtastic-berlin:matrix.org`)
The application derives SEO-friendly document titles, descriptions, and social
preview tags from these existing configuration values and reuses the bundled
logo for Open Graph and Twitter cards.
Example:
```bash

View File

@@ -42,6 +42,14 @@ MAX_JSON_BODY_BYTES = begin
end
VERSION_FALLBACK = "v0.2.1"
def fetch_config_string(key, default)
value = ENV[key]
return default if value.nil?
trimmed = value.strip
trimmed.empty? ? default : trimmed
end
def determine_app_version
repo_root = File.expand_path("..", __dir__)
git_dir = File.join(repo_root, ".git")
@@ -71,15 +79,85 @@ APP_VERSION = determine_app_version
set :public_folder, File.join(__dir__, "public")
set :views, File.join(__dir__, "views")
SITE_NAME = ENV.fetch("SITE_NAME", "Meshtastic Berlin")
DEFAULT_CHANNEL = ENV.fetch("DEFAULT_CHANNEL", "#MediumFast")
DEFAULT_FREQUENCY = ENV.fetch("DEFAULT_FREQUENCY", "868MHz")
SITE_NAME = fetch_config_string("SITE_NAME", "Meshtastic Berlin")
DEFAULT_CHANNEL = fetch_config_string("DEFAULT_CHANNEL", "#MediumFast")
DEFAULT_FREQUENCY = fetch_config_string("DEFAULT_FREQUENCY", "868MHz")
MAP_CENTER_LAT = ENV.fetch("MAP_CENTER_LAT", "52.502889").to_f
MAP_CENTER_LON = ENV.fetch("MAP_CENTER_LON", "13.404194").to_f
MAX_NODE_DISTANCE_KM = ENV.fetch("MAX_NODE_DISTANCE_KM", "137").to_f
MATRIX_ROOM = ENV.fetch("MATRIX_ROOM", "#meshtastic-berlin:matrix.org")
DEBUG = ENV["DEBUG"] == "1"
def sanitized_string(value)
value.to_s.strip
end
def sanitized_site_name
sanitized_string(SITE_NAME)
end
def sanitized_default_channel
sanitized_string(DEFAULT_CHANNEL)
end
def sanitized_default_frequency
sanitized_string(DEFAULT_FREQUENCY)
end
def sanitized_matrix_room
value = sanitized_string(MATRIX_ROOM)
value.empty? ? nil : value
end
def sanitized_max_distance_km
return nil unless defined?(MAX_NODE_DISTANCE_KM)
distance = MAX_NODE_DISTANCE_KM
return nil unless distance.is_a?(Numeric)
return nil unless distance.positive?
distance
end
def formatted_distance_km(distance)
format("%.1f", distance).sub(/\.0\z/, "")
end
def meta_description
site = sanitized_site_name
channel = sanitized_default_channel
frequency = sanitized_default_frequency
matrix = sanitized_matrix_room
summary = "Live Meshtastic mesh map for #{site}"
if channel.empty? && frequency.empty?
summary += "."
elsif channel.empty?
summary += " tuned to #{frequency}."
elsif frequency.empty?
summary += " on #{channel}."
else
summary += " on #{channel} (#{frequency})."
end
sentences = [summary, "Track nodes, messages, and coverage in real time."]
if (distance = sanitized_max_distance_km)
sentences << "Shows nodes within roughly #{formatted_distance_km(distance)} km of the map center."
end
sentences << "Join the community in #{matrix} on Matrix." if matrix
sentences.join(" ")
end
def meta_configuration
site = sanitized_site_name
{
title: site,
name: site,
description: meta_description,
}
end
class << Sinatra::Application
def apply_logger_level!
logger = settings.logger
@@ -569,14 +647,19 @@ end
#
# Renders the main site with configuration-driven defaults for the template.
get "/" do
meta = meta_configuration
erb :index, locals: {
site_name: SITE_NAME,
default_channel: DEFAULT_CHANNEL,
default_frequency: DEFAULT_FREQUENCY,
site_name: meta[:name],
meta_title: meta[:title],
meta_name: meta[:name],
meta_description: meta[:description],
default_channel: sanitized_default_channel,
default_frequency: sanitized_default_frequency,
map_center_lat: MAP_CENTER_LAT,
map_center_lon: MAP_CENTER_LON,
max_node_distance_km: MAX_NODE_DISTANCE_KM,
matrix_room: MATRIX_ROOM,
matrix_room: sanitized_matrix_room,
version: APP_VERSION,
}
end

View File

@@ -189,6 +189,23 @@ RSpec.describe "Potato Mesh Sinatra app" do
get "/"
expect(last_response.body).to include("#{APP_VERSION}")
end
it "includes SEO metadata from configuration" do
stub_const("SITE_NAME", "Spec Mesh Title")
stub_const("DEFAULT_CHANNEL", "#SpecChannel")
stub_const("DEFAULT_FREQUENCY", "915MHz")
stub_const("MAX_NODE_DISTANCE_KM", 120.5)
stub_const("MATRIX_ROOM", " #spec-room:example.org ")
expected_description = "Live Meshtastic mesh map for Spec Mesh Title on #SpecChannel (915MHz). Track nodes, messages, and coverage in real time. Shows nodes within roughly 120.5 km of the map center. Join the community in #spec-room:example.org on Matrix."
get "/"
expect(last_response.body).to include(%(meta name="description" content="#{expected_description}" />))
expect(last_response.body).to include('<meta property="og:title" content="Spec Mesh Title" />')
expect(last_response.body).to include('<meta property="og:site_name" content="Spec Mesh Title" />')
expect(last_response.body).to include('<meta name="twitter:image" content="http://example.org/potatomesh-logo.svg" />')
end
end
describe "database initialization" do

View File

@@ -20,7 +20,32 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title><%= site_name %></title>
<% meta_title_html = Rack::Utils.escape_html(meta_title) %>
<% meta_name_html = Rack::Utils.escape_html(meta_name) %>
<% meta_description_html = Rack::Utils.escape_html(meta_description) %>
<% request_path = request.path.to_s.empty? ? "/" : request.path %>
<% canonical_url = "#{request.base_url}#{request_path}" %>
<% canonical_html = Rack::Utils.escape_html(canonical_url) %>
<% logo_url = "#{request.base_url}/potatomesh-logo.svg" %>
<% logo_url_html = Rack::Utils.escape_html(logo_url) %>
<% logo_alt_html = Rack::Utils.escape_html("#{meta_name} logo") %>
<title><%= meta_title_html %></title>
<meta name="application-name" content="<%= meta_name_html %>" />
<meta name="apple-mobile-web-app-title" content="<%= meta_name_html %>" />
<meta name="description" content="<%= meta_description_html %>" />
<link rel="canonical" href="<%= canonical_html %>" />
<meta property="og:title" content="<%= meta_title_html %>" />
<meta property="og:site_name" content="<%= meta_name_html %>" />
<meta property="og:description" content="<%= meta_description_html %>" />
<meta property="og:type" content="website" />
<meta property="og:url" content="<%= canonical_html %>" />
<meta property="og:image" content="<%= logo_url_html %>" />
<meta property="og:image:alt" content="<%= logo_alt_html %>" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="<%= meta_title_html %>" />
<meta name="twitter:description" content="<%= meta_description_html %>" />
<meta name="twitter:image" content="<%= logo_url_html %>" />
<meta name="twitter:image:alt" content="<%= logo_alt_html %>" />
<link rel="icon" type="image/svg+xml" href="/potatomesh-logo.svg" />
<% refresh_interval_seconds = 60 %>
<% tile_filter_light = "grayscale(1) saturate(0) brightness(0.92) contrast(1.05)" %>