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`.
A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card
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's multiplayer screen, with live status, player count, MOTD and version pulled
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
it sends `DENY` and can't be configured. Always verify headers before enabling
`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,
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`

View file

@ -27,6 +27,8 @@ interface Shortcut {
image?: string;
/** Show the real site in an iframe. Only for sites that allow framing. */
embed?: boolean;
/** Frame src when it differs from `url` (e.g. a local mirror of a blocking site). */
embedUrl?: string;
}
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 },
];
// 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) => ({
id: `friend-${i}`,
label: f.name,
url: f.realUrl || f.url,
embedUrl: f.url,
embed: f.iframe,
color: "#c0392b",
icon: "🫂",
image: f.img,
@ -214,6 +224,7 @@ export default function WindowsDesktop() {
icon: s.icon,
image: s.image,
embed: s.embed,
embedUrl: s.embedUrl,
});
setDynamicApps((prev) => (prev[id] ? prev : { ...prev, [id]: 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.
*/
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 {
return {
id: opts.id,
@ -861,7 +867,7 @@ export function createWebsiteApp(opts: {
height: opts.embed ? 640 : 440,
url: opts.url,
content: opts.embed ? (
<EmbeddedSite title={opts.title} url={opts.url} />
<EmbeddedSite title={opts.title} url={opts.embedUrl ?? opts.url} />
) : (
<WebsiteApp
title={opts.title}