aboutsummaryrefslogtreecommitdiffstats
path: root/build.js
diff options
context:
space:
mode:
Diffstat (limited to 'build.js')
-rw-r--r--build.js51
1 files changed, 48 insertions, 3 deletions
diff --git a/build.js b/build.js
index f97907f..838e891 100644
--- a/build.js
+++ b/build.js
@@ -14,7 +14,7 @@ import { mkdir, readdir } from 'node:fs/promises';
const FAVICON = 'data:image/svg+xml,%3Csvg%20width%3D%2272%22%20height%3D%2272%22%20viewBox%3D%220%200%2072%2072%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%20%3Cpath%20d%3D%22M69.8824%2036C69.8824%2054.7127%2054.7127%2069.8824%2036%2069.8824C17.2873%2069.8824%202.11765%2054.7127%202.11765%2036C2.11765%2017.2873%2017.2873%202.11765%2036%202.11765C54.7127%202.11765%2069.8824%2017.2873%2069.8824%2036Z%22%20fill%3D%22white%22/%3E%20%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M36%204.23529C18.4568%204.23529%204.23529%2018.4568%204.23529%2036C4.23529%2053.5432%2018.4568%2067.7647%2036%2067.7647C53.5432%2067.7647%2067.7647%2053.5432%2067.7647%2036C67.7647%2018.4568%2053.5432%204.23529%2036%204.23529ZM0%2036C0%2016.1177%2016.1177%200%2036%200C55.8823%200%2072%2016.1177%2072%2036C72%2055.8823%2055.8823%2072%2036%2072C16.1177%2072%200%2055.8823%200%2036Z%22%20fill%3D%22black%22/%3E%20%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M36%2058.5882C48.4751%2058.5882%2058.5882%2048.4751%2058.5882%2036C58.5882%2023.5248%2048.4751%2013.4117%2036%2013.4117C23.5249%2013.4117%2013.4118%2023.5248%2013.4118%2036C13.4118%2048.4751%2023.5249%2058.5882%2036%2058.5882ZM36%2054C45.9411%2054%2054%2045.9411%2054%2036C54%2026.0589%2045.9411%2018%2036%2018C26.0589%2018%2018%2026.0589%2018%2036C18%2045.9411%2026.0589%2054%2036%2054Z%22%20fill%3D%22black%22/%3E%20%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M36.0001%2022.8988C36.0001%2022.8988%2036%2022.8988%2036%2022.8988C28.7644%2022.8988%2022.8988%2028.7644%2022.8988%2036C22.8988%2043.2356%2028.7644%2049.1012%2036%2049.1012C36%2049.1012%2036.0001%2049.1012%2036.0001%2049.1012V22.8988Z%22%20fill%3D%22black%22/%3E%20%3C/svg%3E';
// Static assets copied verbatim into dist/ (icons referenced by the manifest).
-const STATIC = ['icon-192.png', 'icon-512.png', 'icon-maskable-512.png', 'icon.svg', 'badge-96.png', 'PublicSans.woff2', 'PublicSans-Italic.woff2', 'PublicSans-OFL.txt'];
+const STATIC = ['icon-192.png', 'icon-512.png', 'icon-maskable-512.png', 'icon.svg', 'badge-96.png', 'og-image.png', 'PublicSans.woff2', 'PublicSans-Italic.woff2', 'PublicSans-OFL.txt'];
const MANIFEST = {
id: '/',
@@ -86,6 +86,12 @@ self.addEventListener('fetch', (e) => {
// Feed snapshots have their own short HTTP lifetime; never freeze one in Cache Storage.
if (u.pathname === '/api/feed') return;
+ // Streams: a live playlist grows every two seconds (frozen at its first
+ // copy, the player stalled and failed), and video segments would fill the
+ // cache with what's already been watched.
+ if (u.pathname.startsWith('/live/') || u.pathname.startsWith('/vmedia/hls')) return;
+ // The developer docs are their own page, not the app: never the offline shell.
+ if (u.pathname === '/docs' || u.pathname.startsWith('/docs/')) return;
if (req.mode === 'navigate') {
// A note page comes back with its thread rendered into it (the server's
@@ -153,6 +159,15 @@ const SW_REGISTER =
`navigator.serviceWorker.addEventListener('controllerchange',function(){_ask(navigator.serviceWorker.controller)});` +
`addEventListener('load',function(){navigator.serviceWorker.register('/sw.js',{updateViaCache:'none'}).catch(function(){})})}</script>`;
+// Crash-loop guard. iOS kills a page that dies twice in a row while opening
+// ("A problem repeatedly occurred") and the wallet is locked out with nothing
+// to go on. Count page starts here, before the app runs; the app clears the
+// count once it has lived a while or is left cleanly (hidden / pagehide) and
+// notes the boot phase it reaches. Three starts with no clear = the app
+// opens in safe mode: no automatic connections, a notice naming the last
+// phase, and a way back to a normal boot.
+export const BOOT_GUARD = `<script>try{var _bk='coinos-boot',_bn=+(localStorage.getItem(_bk)||0)+1;localStorage.setItem(_bk,String(_bn));window.__bootPhase=localStorage.getItem('coinos-boot-phase')||'';if(_bn>=3)window.__safeBoot=_bn;}catch(e){}</script>`;
+
// Lazy bundles load as classic scripts into the page's global scope, where
// the main bundle's minified top-level names already live — two Bun outputs
// can mint the same identifier and die with "already been declared". An IIFE
@@ -350,7 +365,9 @@ async function bundleApp({ minify, features, staging, splitting = false }) {
// (Mediabunny, ~700 KB) would ride along in a file meant to be small
// and offline — there it uploads the original, as before
...(splitting ? [] : [{ name: 'no-video-compress', setup(build) {
- build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; }' }));
+ build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; } export async function streamVideo() { return null; } export const STREAM_MIN_S = 90;' }));
+ // likewise hls.js (~400 KB): Firefox's player for live streams
+ build.onLoad({ filter: /src\/hls-player\.js$/ }, () => ({ loader: 'js', contents: 'export async function attachHls() { return null; }' }));
} }])],
});
if (!result.success) {
@@ -386,21 +403,47 @@ const BOOT_SHELL_SCRIPT = `<script>try{var m=location.pathname.match(/^\\/([A-Za
// fetch the live site over the top of them.
const NO_SW = !!env('NO_SW');
+// What a link to the site unfurls as (chat apps, social posts, search). The
+// server-rendered note and profile pages swap this block for their own
+// (feed/note-page.js siteTagsOut), so the markers must stay.
+const SHARE_IMAGE = 'https://v3.coinos.io/og-image.png';
+const SHARE_DESC = 'Self-custody Bitcoin wallet in your browser: Lightning, Ark and Nostr, with your own keys.';
+const siteTags = (staging) => `<!--site-tags-->
+<meta name="description" content="${SHARE_DESC}">
+<meta property="og:type" content="website">
+<meta property="og:site_name" content="Coinos">
+<meta property="og:title" content="${staging ? 'Coinos Staging' : 'Coinos'}">
+<meta property="og:description" content="${SHARE_DESC}">
+<meta property="og:image" content="${SHARE_IMAGE}">
+<meta property="og:image:width" content="1200">
+<meta property="og:image:height" content="630">
+<meta property="og:image:alt" content="The coinos logo">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:image" content="${SHARE_IMAGE}">
+<!--/site-tags-->
+`;
+
async function pageHtml({ css, staging, pwa, scriptHtml }) {
+ // On https the page asks for its http:// pictures and clips (and ws://
+ // relays) over https: a post's one http:// clip made Chrome call the whole
+ // page "not secure" (displayed mixed content), and the Android app (a TWA)
+ // then showed the address bar. Not on http (local test servers).
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
+<script>if(location.protocol==='https:')document.write('<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">')</script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, interactive-widget=resizes-content">
<meta name="color-scheme" content="light dark">
<title>${staging ? 'Coinos Staging' : 'Coinos'}</title>
<link rel="icon" href="${FAVICON}">
-${pwa ? PWA_HEAD : ''}<script>try{var t=localStorage.getItem('btc-wallet-theme');if(t!=='dark'&&t!=='light')t=matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';document.documentElement.dataset.theme=t;var m=document.querySelector('meta[name="theme-color"]');if(m)m.setAttribute('content',t==='dark'?'#15171a':'#eef0f3');}catch(e){}</script>
+${siteTags(staging)}${pwa ? PWA_HEAD : ''}<script>try{var t=localStorage.getItem('btc-wallet-theme');if(t!=='dark'&&t!=='light')t=matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';document.documentElement.dataset.theme=t;var m=document.querySelector('meta[name="theme-color"]');if(m)m.setAttribute('content',t==='dark'?'#15171a':'#eef0f3');}catch(e){}</script>
<style>${css}</style>
</head>
<body>
<div id="app">${bootShell(await brandSvg(), staging)}</div>
${BOOT_SHELL_SCRIPT}
+${BOOT_GUARD}
${scriptHtml}
${pwa && !NO_SW ? SW_REGISTER + '\n' : ''}</body>
</html>`;
@@ -524,6 +567,8 @@ if (import.meta.main) {
// ...and the default profile covers, full size and the wizard's preview copies
for (const d of ['punks', 'punks-sm', 'covers', 'covers-sm'])
for (const f of readdirSync('static/' + d)) await Bun.write(`dist/${d}/${f}`, Bun.file(`static/${d}/${f}`));
+ // the developer docs, a plain page at /docs/
+ for (const f of readdirSync('static/docs')) await Bun.write(`dist/docs/${f}`, Bun.file(`static/docs/${f}`));
const kb = (Buffer.byteLength(html) / 1024).toFixed(0);
const jkb = (Buffer.byteLength(rawJs) / 1024).toFixed(0);