mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
Derive SEO metadata from existing config (#153)
This commit is contained in:
@@ -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
|
||||
|
||||
97
web/app.rb
97
web/app.rb
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)" %>
|
||||
|
||||
Reference in New Issue
Block a user