embed friends' sites live in desktop windows; frame the skopec mirror since skopec.dev blocks framing

This commit is contained in:
Martin Dvořák 2026-09-30 06:30:30 +00:00
commit d5dfea060f
4 changed files with 30 additions and 1 deletions

View file

@ -41,6 +41,10 @@ heatmap**, a gym SBD panel and links to friends' sites.
`ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL` and Forgejo needs `[cors] X_FRAME_OPTIONS = unset`. `ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL` and Forgejo needs `[cors] X_FRAME_OPTIONS = unset`.
A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card
instead. instead.
- **Friend sites in windows** — clicking a friend's desktop icon opens their site in a
window. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they load live;
`skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so the window frames the bundled mirror
at `/skopec.html` and the `www` button opens the real site.
- **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on - **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on
Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled
from mcstatus.io through the site's own ping API. from mcstatus.io through the site's own ping API.

View file

@ -295,6 +295,14 @@ list reordering, not free placement. Don't confuse the two.
`Content-Security-Policy: frame-ancestors`. Discord keeps a link card because `Content-Security-Policy: frame-ancestors`. Discord keeps a link card because
it sends `DENY` and can't be configured. Always verify headers before enabling it sends `DENY` and can't be configured. Always verify headers before enabling
`embed` on a new service. `embed` on a new service.
- Friend shortcuts reuse the same embed path, driven by `data.ts`. `friend.iframe`
is the `embed` flag; `friend.url` is what the frame loads and `friend.realUrl`
is what the www button opens — `createWebsiteApp`'s `embedUrl` option lets the
two differ. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they
frame the live site; `skopec.dev` sends `X-Frame-Options: SAMEORIGIN` on every
path, so `friends[0].url` points at the bundled `/public/skopec.html` mirror
and only the button is live. Making a blocking site load *live* would need a
header-stripping proxy (allowlisted) — not implemented.
- **Embedded iframes swallow the pointer.** While dragging/resizing a window, - **Embedded iframes swallow the pointer.** While dragging/resizing a window,
the moment the cursor passes over a service `<iframe>` the site's own document the moment the cursor passes over a service `<iframe>` the site's own document
receives the `pointermove`/`pointerup`, so a gesture that listens on `window` receives the `pointermove`/`pointerup`, so a gesture that listens on `window`

View file

@ -27,6 +27,8 @@ interface Shortcut {
image?: string; image?: string;
/** Show the real site in an iframe. Only for sites that allow framing. */ /** Show the real site in an iframe. Only for sites that allow framing. */
embed?: boolean; embed?: boolean;
/** Frame src when it differs from `url` (e.g. a local mirror of a blocking site). */
embedUrl?: string;
} }
interface IconEntry { interface IconEntry {
@ -58,10 +60,18 @@ const SERVICE_SHORTCUTS: Shortcut[] = [
{ id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT", embed: true }, { id: "svc-gitdxd", icon: "🐙", label: "GitDXD", url: "https://git.dvorakmortonxd.dev", color: "#6a4fb3", desc: "Best Git Hosting OAT", embed: true },
]; ];
// Friends' sites, opened in a live window when they allow framing. bukovinafilip
// and vlcak.me send no framing headers, so `url` (== `realUrl`) is framed
// directly. skopec.dev sends `X-Frame-Options: SAMEORIGIN` on every path, so we
// frame the same-origin mirror at `/skopec.html` instead; the www button still
// opens the live site. To embed a site that blocks framing live, it would need a
// header-stripping proxy.
const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({ const FRIEND_SHORTCUTS: Shortcut[] = friends.map((f, i) => ({
id: `friend-${i}`, id: `friend-${i}`,
label: f.name, label: f.name,
url: f.realUrl || f.url, url: f.realUrl || f.url,
embedUrl: f.url,
embed: f.iframe,
color: "#c0392b", color: "#c0392b",
icon: "🫂", icon: "🫂",
image: f.img, image: f.img,
@ -214,6 +224,7 @@ export default function WindowsDesktop() {
icon: s.icon, icon: s.icon,
image: s.image, image: s.image,
embed: s.embed, embed: s.embed,
embedUrl: s.embedUrl,
}); });
setDynamicApps((prev) => (prev[id] ? prev : { ...prev, [id]: app })); setDynamicApps((prev) => (prev[id] ? prev : { ...prev, [id]: app }));
openAppDef(app); openAppDef(app);

View file

@ -851,6 +851,12 @@ export function createWebsiteApp(opts: {
* sites that do not send `X-Frame-Options` / a `frame-ancestors` CSP. * sites that do not send `X-Frame-Options` / a `frame-ancestors` CSP.
*/ */
embed?: boolean; embed?: boolean;
/**
* URL to load inside the frame when it differs from `url` (which the www
* button opens). Used for sites that block framing: we frame a same-origin
* mirror instead, while the button still points at the live site.
*/
embedUrl?: string;
}): AppDef { }): AppDef {
return { return {
id: opts.id, id: opts.id,
@ -861,7 +867,7 @@ export function createWebsiteApp(opts: {
height: opts.embed ? 640 : 440, height: opts.embed ? 640 : 440,
url: opts.url, url: opts.url,
content: opts.embed ? ( content: opts.embed ? (
<EmbeddedSite title={opts.title} url={opts.url} /> <EmbeddedSite title={opts.title} url={opts.embedUrl ?? opts.url} />
) : ( ) : (
<WebsiteApp <WebsiteApp
title={opts.title} title={opts.title}