Changeset ef519a3 in Klonkt


Ignore:
Timestamp:
07/30/2026 11:32:06 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
2a17f0a
Parents:
1a2f206
Message:

Volg-QR draagt een https-URL met een tussenpagina

Robins melding (31-7): de camera-app kan de QR wel lezen maar biedt een
onbekend scheme niet aan om te openen; Google Lens behandelt alles
behalve https als platte tekst. De QR codeert nu de https-URL van een
kleine tussenpagina (/ap/users/:slug/follow) met een grote Open in
Shaer-knop die alsnog share:social/follow/AP/@handle afvuurt: vanuit de
browser werkt het custom scheme wel (BROWSABLE intent-filter; Safari
prompt op iOS). Wie geen Shaer heeft ziet de handle gewoon staan.

Changed files:
src/routes/activitypub.js

  • follow-qr.png codeert de https-URL van de tussenpagina
  • /ap/users/:slug/follow: de tussenpagina (zelfstandige html, geen externe assets)

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r1a2f206 ref519a3  
    124124
    125125// ── Follow-QR (Robins verzoek, 31-7) ──────────────────────────────
    126 // A PNG QR of share:social/follow/AP/@slug@host: the ward shows it in
    127 // Account, a friend scans the SCREEN with the ordinary camera app and their
    128 // Shaer opens with the follow question. Public on purpose: it encodes only
    129 // the public handle, and the app's plain image loaders carry no bearer.
     126// The QR carries an HTTPS url, not the share: scheme: camera apps (Google
     127// Lens voorop) treat unknown schemes as plain text and only offer to OPEN
     128// https links (Robins melding, 31-7). The url lands on the interstitial
     129// below, whose one big button fires the share: scheme — from a browser the
     130// custom scheme DOES work (BROWSABLE intent-filter; Safari prompts).
     131// Public on purpose: it encodes only the public handle, and the app's plain
     132// image loaders carry no bearer.
    130133router.get('/ap/users/:slug/follow-qr.png', async (req, res) => {
    131134  const site = db.prepare('SELECT slug FROM sites WHERE slug = ?').get(req.params.slug);
    132135  if (!site) return res.status(404).end();
    133136  try {
    134     const host = new URL(baseUrl(req)).host;
    135137    const { default: QRCode } = await import('qrcode');
    136     const png = await QRCode.toBuffer(`share:social/follow/AP/@${site.slug}@${host}`, { width: 600, margin: 1 });
     138    const png = await QRCode.toBuffer(`${baseUrl(req)}/ap/users/${encodeURIComponent(site.slug)}/follow`, { width: 600, margin: 1 });
    137139    res.set('Content-Type', 'image/png');
    138140    res.set('Cache-Control', 'public, max-age=86400');
     
    142144    res.status(500).end();
    143145  }
     146});
     147
     148// The interstitial the QR opens: one big button into Shaer, and the handle
     149// in plain sight for whoever has no Shaer (yet).
     150router.get('/ap/users/:slug/follow', (req, res) => {
     151  const site = db.prepare('SELECT slug, title FROM sites WHERE slug = ?').get(req.params.slug);
     152  if (!site) return res.status(404).end();
     153  const host = new URL(baseUrl(req)).host;
     154  const esc = (t) => String(t).replace(/[<>&"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c]));
     155  const handle = `@${site.slug}@${host}`;
     156  const name = esc(site.title || site.slug);
     157  res.set('Cache-Control', 'public, max-age=3600');
     158  res.send(`<!doctype html><html lang="en"><head><meta charset="utf-8">
     159<meta name="viewport" content="width=device-width, initial-scale=1">
     160<title>Follow ${name}</title>
     161<style>
     162  body { font-family: system-ui, sans-serif; margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
     163         background: linear-gradient(160deg, #5A32E6, #2a1a5e); color: #fff; text-align: center; }
     164  main { padding: 32px; max-width: 420px; }
     165  h1 { font-size: 1.5rem; margin: 0 0 .4rem; }
     166  .handle { opacity: .85; font-family: ui-monospace, monospace; word-break: break-all; }
     167  a.go { display: block; margin: 28px auto 14px; padding: 16px 28px; border-radius: 999px; background: #fff; color: #2a1a5e;
     168         font-weight: 700; font-size: 1.15rem; text-decoration: none; }
     169  p.small { font-size: .85rem; opacity: .75; line-height: 1.5; }
     170</style></head><body><main>
     171  <h1>Follow ${name}</h1>
     172  <div class="handle">${esc(handle)}</div>
     173  <a class="go" href="share:social/follow/AP/${esc(handle)}">Open in Shaer</a>
     174  <p class="small">No Shaer? Any fediverse app can follow ${esc(handle)}.</p>
     175</main></body></html>`);
    144176});
    145177
Note: See TracChangeset for help on using the changeset viewer.