Files
VoxVera/site/index.html
T
thePR0M3TH3ANandClaude Opus 4.8 6fe9069f82 Add Open Graph preview image (1200x630 card) site-wide
Shared voxvera.org links now render a real image card, not just a text
summary. Adds site/og-card.jpg (1200x630, ~90KB, derived from the
"say something… voxvera.org" custom art) and wires og:image +
twitter:image + image dimensions/alt on the home, board, and safety
pages, bumping twitter:card to summary_large_image.

Per-flyer previews remain out of scope (fragment URLs are invisible to
servers; this stays a purely static site). Test asserts all three pages
declare the og:image and that the asset is served as JPEG. Full suite
378 passing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 22:27:03 -04:00

282 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VoxVera Nostr Client</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="nostr-client.css">
<meta name="description" content="Create printable flyers backed by decentralized Nostr events. Anonymous by default, and no single host can quietly take them down.">
<meta property="og:type" content="website">
<meta property="og:site_name" content="VoxVera">
<meta property="og:title" content="VoxVera — printable, censorship-resistant flyers">
<meta property="og:description" content="Create printable flyers backed by decentralized Nostr events. Anonymous by default, and no single host can quietly take them down.">
<meta property="og:url" content="https://voxvera.org/">
<meta property="og:image" content="https://voxvera.org/og-card.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="VoxVera — say something. voxvera.org">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="VoxVera — printable, censorship-resistant flyers">
<meta name="twitter:description" content="Create printable flyers backed by decentralized Nostr events. Anonymous by default, and no single host can quietly take them down.">
<meta name="twitter:image" content="https://voxvera.org/og-card.jpg">
</head>
<body>
<header class="topbar">
<div>
<h1 id="client-title">VoxVera Nostr Client</h1>
<p id="client-description">Static editor and viewer for decentralized flyer source events.</p>
</div>
<div class="topbar-controls">
<nav class="mode-tabs" aria-label="Client mode">
<button id="editor-tab" class="tab active" type="button" data-mode="editor">Editor</button>
<button id="viewer-tab" class="tab" type="button" data-mode="viewer">Viewer</button>
</nav>
<select id="topbar-lang" aria-label="Language"></select>
<select id="topbar-paper" aria-label="Paper size"></select>
</div>
</header>
<div class="viewer-floating" aria-label="Viewer controls">
<button type="button" id="open-viewer-controls">Load Event</button>
<button type="button" id="viewer-print">Print</button>
<button type="button" id="viewer-editor">Editor</button>
<select id="viewer-lang" aria-label="Language"></select>
<select id="viewer-paper" aria-label="Paper size"></select>
</div>
<main>
<section id="editor-panel" class="panel active" aria-labelledby="editor-heading">
<div class="workspace">
<form id="editor-form" class="editor-form">
<div class="section-title">
<h2 id="editor-heading">Editor</h2>
<span id="signer-state" class="state-pill">Checking signer</span>
</div>
<label>
<span id="label-relays">Relays</span>
<textarea id="editor-relays" rows="3" spellcheck="false"></textarea>
</label>
<div class="identity-row">
<span id="identity-state" class="state-pill">Checking identity</span>
</div>
<div class="identity-controls">
<button type="button" id="generate-anon-identity" class="secondary-button">Generate anonymous npub</button>
<button type="button" id="connect-nip07" class="secondary-button">Connect extension</button>
<button type="button" id="use-nsec-toggle" class="secondary-button">Use a private key (nsec)</button>
<button type="button" id="use-nip46-toggle" class="secondary-button">Remote signer (NIP-46)</button>
</div>
<!-- Import a private key. The secret signs in-page; it is only written
to storage (encrypted with the PIN) when "Remember" is checked. -->
<div id="nsec-import" class="identity-import" hidden>
<input id="nsec-input" type="password" autocomplete="off" spellcheck="false" placeholder="nsec1…">
<label class="nsec-remember-row">
<input type="checkbox" id="nsec-remember">
<span data-i18n-key="remember_pin">Remember on this device (PIN)</span>
</label>
<input id="nsec-pin" type="password" inputmode="numeric" autocomplete="off" placeholder="PIN (4+ digits)" hidden>
<button type="button" id="nsec-submit" class="secondary-button">Use this key</button>
</div>
<!-- Connect a NIP-46 remote signer (bunker). The local key is
ephemeral and in-memory only; the user's secret stays in their
signer app. Session-only: not persisted across reloads. -->
<div id="nip46-import" class="identity-import" hidden>
<span id="nip46-hint" class="board-key-note" data-i18n-key="nip46_hint">Paste a bunker:// link from your signer app.</span>
<input id="nip46-input" type="text" autocomplete="off" spellcheck="false" placeholder="bunker://…">
<button type="button" id="nip46-submit" class="secondary-button">Connect</button>
<span id="nip46-status" class="board-key-note" role="status"></span>
<button type="button" id="nip46-scan" class="secondary-button">Scan with your signer app</button>
<!-- nostrconnect:// QR: the client generates the link; the signer app
scans it and connects back. The local key is ephemeral. -->
<div id="nip46-qr-wrap" hidden>
<div id="nip46-qr" class="nip46-qr"></div>
<input id="nip46-uri" readonly>
<button type="button" id="nip46-uri-copy" class="secondary-button">Copy link</button>
<span id="nip46-scan-status" class="board-key-note" role="status"></span>
</div>
</div>
<!-- Shown on load when a PIN-encrypted identity is stored. -->
<div id="identity-locked-row" class="identity-import" hidden>
<span id="identity-locked-note" data-i18n-key="identity_locked">Saved identity is locked. Enter your PIN to unlock it.</span>
<input id="unlock-pin" type="password" inputmode="numeric" autocomplete="off" placeholder="PIN">
<button type="button" id="unlock-key" class="secondary-button">Unlock</button>
<button type="button" id="forget-key" class="secondary-button">Forget</button>
</div>
<p id="identity-error" class="field-error" role="alert" hidden></p>
<div class="field-grid">
<label>
<span id="label-flyer-name">Flyer Name</span>
<input id="field-folder-name" name="folder_name" maxlength="64" value="voxvera">
</label>
<label>
<span id="label-language">Language</span>
<select id="field-lang" name="lang">
<option value="en">en</option>
<option value="es">es</option>
<option value="de">de</option>
<option value="fr">fr</option>
<option value="ru">ru</option>
<option value="he">he</option>
<option value="ar">ar</option>
<option value="ja">ja</option>
<option value="hi">hi</option>
<option value="sw">sw</option>
<option value="pt">pt</option>
<option value="tr">tr</option>
<option value="zh">zh</option>
<option value="fa">fa</option>
</select>
</label>
</div>
<label>
<span id="label-page-title">Page title</span>
<input id="field-name" name="name" maxlength="120" value="Vox Vera Printable Flyers">
</label>
<label>
<span id="label-title">Title</span>
<input id="field-title" name="title" maxlength="80" value="TOP SECRET">
</label>
<label>
<span id="label-subtitle">Subtitle</span>
<input id="field-subtitle" name="subtitle" maxlength="120" value="DO ~~NOT~~ DISTRIBUTE">
</label>
<label>
<span id="label-headline">Headline</span>
<input id="field-headline" name="headline" maxlength="160" value="OPERATION VOX VERA">
</label>
<label>
<span id="label-content">Content</span>
<textarea id="field-content" name="content" rows="9" maxlength="10000"></textarea>
</label>
<label>
<span id="label-url-message">URL message</span>
<input id="field-url-message" name="url_message" maxlength="240" value="Follow this link to learn more.">
</label>
<label>
<span id="label-poster-url">Content URL</span>
<input id="field-url" name="url" maxlength="2048" value="https://voxvera.org/">
</label>
<label>
<span id="label-footer-message">Footer message</span>
<input id="field-footer-message" name="footer_message" maxlength="240" value="0110010 0101011 0110010 0111101 0110100">
</label>
<div class="actions">
<button type="button" id="preview-editor">Preview</button>
<button type="button" id="import-design">Import design</button>
<button type="button" id="export-design">Export design</button>
<button type="submit" id="publish-event">Sign and publish</button>
</div>
<!-- Hidden picker for "Import design"; the design file is content +
relays only (no identity/naddr), so it is portable across keys. -->
<input type="file" id="import-design-input" accept="application/json,.json" hidden>
<!-- Shown only when a flyer you authored is loaded from an event. -->
<div id="loaded-flyer-actions" class="actions" hidden>
<button type="button" id="republish-flyer" class="secondary-button" data-i18n-key="republish">Re-publish to relays</button>
<button type="button" id="delete-flyer" class="secondary-button danger" data-i18n-key="delete_flyer">Delete flyer</button>
</div>
<p id="loaded-flyer-status" class="field-error" role="status" hidden></p>
</form>
<aside class="result-panel">
<h2 id="published-event-heading">Published Event</h2>
<dl class="result-list">
<dt id="result-event-id-label">Event ID</dt>
<dd class="copy-value"><code id="event-id-output">Not published</code><button type="button" class="copy-button" data-copy-target="event-id-output">Copy</button></dd>
<dt id="result-author-npub-label">Author npub</dt>
<dd class="copy-value"><code id="author-npub-output">Not generated</code><button type="button" class="copy-button" data-copy-target="author-npub-output">Copy</button></dd>
<dt id="result-naddr-label">Nostr address</dt>
<dd class="copy-value"><code id="naddr-output">Not generated</code><button type="button" class="copy-button" data-copy-target="naddr-output">Copy</button></dd>
<dt id="result-poster-url-label">Poster URL</dt>
<dd class="copy-value"><code id="poster-url-output">Not generated</code><button type="button" class="copy-button" data-copy-target="poster-url-output">Copy</button></dd>
<dt id="result-status-label">Status</dt>
<dd id="publish-status">Idle</dd>
</dl>
<textarea id="event-json-output" rows="14" spellcheck="false" readonly></textarea>
</aside>
</div>
</section>
<section id="viewer-panel" class="panel viewer-drawer" aria-labelledby="viewer-heading">
<div class="workspace">
<form id="viewer-form" class="editor-form">
<div class="section-title">
<h2 id="viewer-heading">Viewer</h2>
<button type="button" id="close-viewer-controls" class="secondary-button">Close</button>
</div>
<label>
<span id="label-event-id">Event ID</span>
<input id="viewer-event-id" spellcheck="false" placeholder="naddr, event id, note1, nevent1, or nostr: URL">
</label>
<label>
<span id="label-viewer-relays">Relays</span>
<textarea id="viewer-relays" rows="3" spellcheck="false"></textarea>
</label>
<div class="actions">
<button id="viewer-fetch-render" type="submit">Fetch and render</button>
<button type="button" id="copy-viewer-config">Copy config JSON</button>
</div>
<p id="viewer-status" class="status-line">Idle</p>
</form>
<aside class="result-panel">
<h2 id="normalized-config-heading">Normalized Config</h2>
<textarea id="viewer-config-output" rows="16" spellcheck="false" readonly></textarea>
</aside>
</div>
</section>
<section class="preview-band" aria-label="Flyer preview">
<div class="preview-toolbar">
<h2 id="preview-heading">Preview</h2>
<button type="button" id="print-preview">Print preview</button>
</div>
<div id="flyer-preview" class="flyer-preview"></div>
<p class="board-link"><a href="board.html" data-i18n-key="bulletin_board">Bulletin board</a> · <a href="/safety.html" data-i18n-key="safety_link">Safety &amp; privacy</a></p>
</section>
</main>
<div id="publish-modal" class="modal-overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="publish-modal-title">
<h2 id="publish-modal-title" data-i18n-key="published_title">Flyer published</h2>
<p id="publish-modal-message" data-i18n-key="published_message">Copy this URL to see your new flyer:</p>
<div class="modal-url-row">
<input id="publish-modal-url" type="text" readonly aria-label="Flyer URL">
<button type="button" id="publish-modal-copy">Copy</button>
</div>
<p id="publish-modal-relays" class="modal-relays"></p>
<button type="button" id="publish-modal-close" class="modal-close" data-i18n-key="close">Close</button>
</div>
</div>
<div id="delete-modal" class="modal-overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="delete-modal-title">
<h2 id="delete-modal-title" data-i18n-key="delete_title">Delete this flyer?</h2>
<p id="delete-modal-message" data-i18n-key="delete_message">This asks the relays to delete it and replaces it with a deletion marker so VoxVera hides it. Deletion can't be guaranteed across every relay.</p>
<div class="modal-actions">
<button type="button" id="delete-modal-cancel" class="secondary-button" data-i18n-key="cancel">Cancel</button>
<button type="button" id="delete-modal-confirm" class="secondary-button danger" data-i18n-key="delete_confirm">Delete</button>
</div>
</div>
</div>
<div id="import-modal" class="modal-overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="import-modal-title">
<h2 id="import-modal-title" data-i18n-key="import_confirm_title">Import this design?</h2>
<p id="import-modal-message" data-i18n-key="import_confirm_message">This replaces the flyer currently in the editor.</p>
<div class="modal-actions">
<button type="button" id="import-modal-cancel" class="secondary-button" data-i18n-key="cancel">Cancel</button>
<button type="button" id="import-modal-confirm" class="secondary-button" data-i18n-key="import_button">Import</button>
</div>
</div>
</div>
<script src="locales.js"></script>
<script src="vendor/qrcode-generator/qrcode.js"></script>
<script src="vendor/nostr-tools/nostr.bundle.js"></script>
<script src="nostr-client.js"></script>
</body>
</html>