feat: ship a web app (#218)

* feat(orion): add endpoint for link metadata

* refactor: cleanup comments

* wip: plumbing for building a web app

* setup deployment materials for web app

* fix(web): favicon
This commit was merged in pull request #218.
This commit is contained in:
Arjun Patel
2026-05-26 15:37:35 -07:00
committed by GitHub
parent 173c63508a
commit 64f02d1c3b
51 changed files with 1260 additions and 238 deletions
-111
View File
@@ -3,7 +3,6 @@ import path from 'node:path';
import started from 'electron-squirrel-startup';
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
import type { LinkMetadata } from './lib/link-metadata';
import { appConfig } from './config/env';
import { logError } from './lib/errors';
import { safeHandle } from './main/ipc-utils';
@@ -370,106 +369,6 @@ ipcMain.on('autoplay:navigate', (_event, data) => {
}
});
// --- Link metadata ---
const metadataCache = new Map<string, LinkMetadata>();
function getMetaContent(html: string, property: string): string | null {
// Match both property="..." and name="..." attributes
const regex = new RegExp(
`<meta[^>]*(?:property|name)=["']${property}["'][^>]*content=["']([^"']*)["']|<meta[^>]*content=["']([^"']*)["'][^>]*(?:property|name)=["']${property}["']`,
'i',
);
const match = html.match(regex);
return match?.[1] ?? match?.[2] ?? null;
}
function getTitle(html: string): string | null {
const match = html.match(/<title[^>]*>([^<]*)<\/title>/i);
return match?.[1]?.trim() ?? null;
}
function getFavicon(html: string, baseUrl: string): string | null {
const match = html.match(/<link[^>]*rel=["'](?:shortcut )?icon["'][^>]*href=["']([^"']*)["']/i)
?? html.match(/<link[^>]*href=["']([^"']*)["'][^>]*rel=["'](?:shortcut )?icon["']/i);
if (!match?.[1]) {
// Fall back to /favicon.ico
try {
const url = new URL(baseUrl);
return `${url.protocol}//${url.host}/favicon.ico`;
} catch {
return null;
}
}
try {
return new URL(match[1], baseUrl).href;
} catch {
return match[1];
}
}
function resolveUrl(src: string | null, baseUrl: string): string | null {
if (!src) return null;
try {
return new URL(src, baseUrl).href;
} catch {
return src;
}
}
async function fetchLinkMetadata(url: string): Promise<LinkMetadata | null> {
const cached = metadataCache.get(url);
if (cached) return cached;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(url, {
signal: controller.signal,
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; llink/1.0)',
'Accept': 'text/html',
},
redirect: 'follow',
});
clearTimeout(timeout);
if (!response.ok) return null;
// Only read the first ~50KB to get <head> content
const reader = response.body?.getReader();
if (!reader) return null;
let html = '';
const decoder = new TextDecoder();
while (html.length < 50_000) {
const { done, value } = await reader.read();
if (done) break;
html += decoder.decode(value, { stream: true });
}
reader.cancel();
const domain = new URL(url).hostname.replace(/^www\./, '');
const metadata: LinkMetadata = {
url,
title: getMetaContent(html, 'og:title') ?? getTitle(html),
description: getMetaContent(html, 'og:description') ?? getMetaContent(html, 'description'),
image: resolveUrl(getMetaContent(html, 'og:image'), url),
favicon: getFavicon(html, url),
domain,
};
metadataCache.set(url, metadata);
return metadata;
} catch (err) {
// Metadata is a progressive enhancement — keep the null contract, but log
// so upstream failures (DNS, TLS, aborted fetches) aren't invisible.
logError(err, { scope: 'link.fetchMetadata', url });
return null;
}
}
// --- Dock badge ---
ipcMain.on('app:set-dock-badge', (_event, count: number) => {
@@ -480,16 +379,6 @@ ipcMain.on('app:set-dock-badge', (_event, count: number) => {
safeHandle('app:get-version', () => app.getVersion());
safeHandle('link:fetch-metadata', async (_event, url) => {
if (typeof url !== 'string') return null;
try {
new URL(url);
} catch {
return null;
}
return fetchLinkMetadata(url);
});
safeHandle('link:open-external', async (_event, url) => {
if (typeof url !== 'string') return;
// Only allow http(s) URLs for security