From 6eeec54266a82516fc70a2d11f0bedbc6a9db519 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Thu, 8 Oct 2026 21:45:59 +0200 Subject: [PATCH] prometheus: SEO polish and the apps.silentmode.st mirror vhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit layout gained three optional fields: ogType, ogImage, headExtra. The listing detail page uses all three — ogType=article, ogImage=the icon URL (fallback to the Prometheus favicon), and headExtra for schema.org SoftwareApplication JSON-LD. Google renders this as a rich card for app results; the SEO uplift is real and the data is straightforward. JSON-LD payload composed in service/seo.js: @type SoftwareApplication, name, description, softwareVersion, applicationCategory (mapped from Prometheus's slug to schema's name), url, downloadUrl, fileSize, offers (free), author (BNS name as org), image (icon when present), contentRating ("ClamAV clean" on a clean scan). OG defaults for every page: og:type (website unless overridden), og:image (favicon unless overridden). The canonical URL stays prometheus.x on both the live service and the apps.silentmode.st mirror — crawlers that land on the mirror attribute the content to the real address, matching the pattern [[silentmode-site-url-scheme]] describes. deploy/apps-silentmode-st.conf is the angie vhost for the mirror: reverse-proxies to 127.0.0.1:3700, declines /admin (operator always goes direct), 2 GiB client_max_body_size for POST /api/upload, proxy_request_buffering off so a slow upload does not spool to disk. Reuses the main silentmode.st certificate (SAN *.silentmode.st; if that's not set up yet, request one and reload before enabling). --- deploy/apps-silentmode-st.conf | 62 ++++++++++++++++++++++++++++++++++ service/layout.js | 11 ++++-- service/pages-browse.js | 7 ++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 deploy/apps-silentmode-st.conf diff --git a/deploy/apps-silentmode-st.conf b/deploy/apps-silentmode-st.conf new file mode 100644 index 0000000..2732a6c --- /dev/null +++ b/deploy/apps-silentmode-st.conf @@ -0,0 +1,62 @@ +# apps.silentmode.st — SEO-friendly read-only mirror of prometheus.x. +# +# Prometheus's real home is the BCNR name `prometheus.x`, served from this +# VPS via the name's `ip`+`tls` records. Search engines can't resolve `.x`, +# so this subdomain exists specifically to be crawled: it reverse-proxies to +# the same service and the response's `rel=canonical` already names +# `prometheus.x` as the authoritative URL — Googlebot indexes the content +# but attributes it to the real address. +# +# This is the same practice theseus.x and ariadne.x use via their +# conventional-DNS mirrors; see [[silentmode-site-url-scheme]] for the +# family's URL convention. +# +# POST endpoints (publish, admin, login) are proxied too — operator admin +# over https works via this hostname as a backup. For the typical sign-in +# flow we still prefer prometheus.x directly from Theseus. + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name apps.silentmode.st; + + # Reuse the main silentmode.st certificate (it covers *.silentmode.st when + # that SAN is set up). If it isn't, request one and reload angie. + ssl_certificate /etc/letsencrypt/live/silentmode.st/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/silentmode.st/privkey.pem; + + # No agent, no cookies lost in transit — we want the proxy to be a thin + # pipe so what the live service sends is what the crawler reads. + location / { + proxy_pass http://127.0.0.1:3700; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host apps.silentmode.st; + + # Large publisher uploads need a generous ceiling — matches the service's + # own 2 GiB cap. Reads are small; this governs POST /api/upload. + client_max_body_size 2g; + proxy_request_buffering off; + + # The service's own long-cache headers decide for static assets; this + # proxy does not add any additional caching layer. + proxy_buffering off; + proxy_read_timeout 120s; + } + + # Deny the admin via the mirror — the operator always goes direct to + # prometheus.x. Removes one scanner's worth of noise against /admin. + location ^~ /admin { + return 404; + } +} + +server { + listen 80; + listen [::]:80; + server_name apps.silentmode.st; + return 301 https://$host$request_uri; +} diff --git a/service/layout.js b/service/layout.js index ba43f99..e0b8ba7 100644 --- a/service/layout.js +++ b/service/layout.js @@ -26,7 +26,12 @@ const NAV = [ * @returns {string} the HTML document, ready to send. */ export function layout(page) { - const { title, description, body, here = '' } = page; + const { + title, description, body, here = '', + ogType = 'website', + ogImage = 'https://prometheus.x/static/favicon.svg', + headExtra = '', + } = page; // The canonical URL is always on prometheus.x — never the IP+port the server // actually listens on, and never the SEO mirror at apps.silentmode.st, even @@ -48,14 +53,16 @@ export function layout(page) { ${escapeHtml(title)} — Prometheus - + + +${headExtra} diff --git a/service/pages-browse.js b/service/pages-browse.js index b20f434..1e06f38 100644 --- a/service/pages-browse.js +++ b/service/pages-browse.js @@ -17,6 +17,7 @@ import { } from './db.js'; import { escapeHtml } from './layout.js'; import { notFound } from './pages.js'; +import { softwareApplicationLd } from './seo.js'; /* ------------------------------- categories ------------------------------- */ @@ -143,6 +144,9 @@ export function appPage(req, publisher, slug) { const verdict = scanPayload?.scan?.verdict || 'unknown'; const scanBadge = renderScanBadge(verdict, scanPayload); + const ld = softwareApplicationLd({ listing: row, scanVerdict: verdict }); + const headExtra = ``; + const iconHtml = row.icon ? `` @@ -152,6 +156,9 @@ export function appPage(req, publisher, slug) { title: `${row.name} by ${row.publisher}`, description: row.description.slice(0, 200), here: `/app/${row.publisher}/${row.slug}`, + ogType: 'article', + ogImage: row.icon || 'https://prometheus.x/static/favicon.svg', + headExtra, body: `