From d32814732232b98caf2e10af349fa6951081bbfb Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 12 Oct 2026 14:48:43 +0200 Subject: [PATCH] website: the share card shows the product, not just the words The operator went to post the link and found the card out of date: it was the 2026-07-18 design - a headline and two lines of text on a dark field, nothing of the product, and older than everything the site has become since. - og-image.png and og-image-en.png rebuilt around the laptop, with the hero's own poster frame on its screen. - scripts/make_og_card.py: the headline, logo and button are lifted as PIXELS from the previous card and composited with `lighter` onto flat --bg-0, because the site's font is a woff2 and nothing here can draw a glyph with it. A new headline therefore CANNOT be made with this script - new words go back to the design tool and this takes that card as its source. The lede lines are gone; the laptop says what they said. - ?v=2 on every og:image and twitter:image (20 pages): a scraper caches by URL, so a new picture at the old URL would reach nobody. A card already attached to a post still does not change - that post must drop the preview and re-add the link after a re-scrape. --- REUSE.md | 1 + scripts/make_og_card.py | 142 +++++++++++++++++++++++++++++++++ website/CHANGELOG.md | 24 +++++- website/adatkezeles.html | 2 +- website/alkalmazasok.html | 4 +- website/assets/og-image-en.png | Bin 135646 -> 286126 bytes website/assets/og-image.png | Bin 167598 -> 285922 bytes website/biztonsagimentes.html | 4 +- website/en/apps.html | 4 +- website/en/backups.html | 4 +- website/en/contact.html | 4 +- website/en/download.html | 4 +- website/en/faq.html | 4 +- website/en/index.html | 4 +- website/en/services.html | 4 +- website/en/technology.html | 4 +- website/en/testing.html | 4 +- website/feltetelek.html | 2 +- website/gyik.html | 4 +- website/index.html | 4 +- website/kapcsolat.html | 4 +- website/letoltes.html | 4 +- website/szolgaltatasok.html | 4 +- website/technologiak.html | 4 +- website/teszteles.html | 4 +- 25 files changed, 204 insertions(+), 39 deletions(-) create mode 100644 scripts/make_og_card.py diff --git a/REUSE.md b/REUSE.md index d49863df..dae4884a 100644 --- a/REUSE.md +++ b/REUSE.md @@ -173,6 +173,7 @@ | Rule-firing report | scripts/rules_report.py | Reads the `InstructionsLoaded` hook log and cross-references every `.claude/rules/*.md` in the workspace. **The column that matters is the empty one** — a rule that has never fired is mis-globbed or dead (built-but-never-wired, applied to instructions). Prints each silent rule's `paths:` so "wrong glob" is distinguishable from "quiet month". Caveat: the log only covers since the hook was armed. | | Gate script | scripts/site_gates.py | Byte-level mechanical gates (BOM, emoji codepoint ranges, nav/footer diff, analytics, banned tokens, cache-bust); run `python scripts/site_gates.py` after ANY website change; non-zero exit on failure. | | Hero clip builder | scripts/make_hero_clip.py | Assembles the home page's laptop clip from the operator's raw app recordings: scale+crop each to the screen's 1.626:1, crossfade, encode mp4 (h264) + webm (VP9) + a webp poster. **Runs on DooPlex** - Windows has no ffmpeg. NOT part of any build, gate or deploy: the site has no build step, so the output is committed like any other picture. Measured 2026-10-12: GIF is 11x the mp4 for the same seconds and dithers to 256 colours, hence mp4+webm. The frame it plays under is `website/assets/hero-laptop-frame.webp`, whose screen is cut out; the hole's position is in the CSS as percentages. **Every scene plays at its full recorded length** (operator ruling 2026-10-12: the raw file IS the edit); `--seconds N` only caps. One output set (`hero-clip.mp4/.webm`, `hero-poster.webp`) serves both languages; `--lang hu` adds a suffix back if they ever differ. | +| Share-card builder | scripts/make_og_card.py | Rebuilds `og-image.png` / `og-image-en.png` (1200x630) around the laptop: the headline, logo and button are lifted as PIXELS from the previous card and composited with `lighter` onto flat `--bg-0`, because the site's font is a woff2 and nothing here can draw a glyph with it. **A new headline cannot be made with this script** - new words go back to the design tool, and this then takes that card as its source. Bump the `?v=` on the og:image URLs afterwards or no scraper will refetch. | | Fetch-validate-install (shell) | scripts/felhom-host-install.sh `step_agent_install` (~L1108) | `fetch_raw` to mktemp → syntax-check (`bash -n`) → `install -m0755 -o root -g root` → only then activate; guarded-mkfs wrapper installed BEFORE the sudoers that references it (ordering is the safety property). All mutations through `run()` (dry-run aware). | | Install-profile gate (shell) | scripts/felhom-host-install.sh `--mode appliance\|byo` (GL-2, v1.10.0) | Mandatory-flag profile (no default), refusals at argv time BEFORE any prompt/step, risky step gated at its CALL SITE (one auditable place — never a branch inside the step), mode persisted to state.json + resume-mismatch refusal, `FELHOM_INSTALL_STATE_DIR` override for harness isolation. Harness: scripts/hostinstall-mode-harness.sh (static refusal matrix + grep-invariants + PVE dry-transcript tier; red-proofs run against a mutated scratch copy). | | Disclosure↔uninstall parity (shell) | scripts/felhom-host-install.sh `_uninstall_statement` + harness GL4-D (v1.11.0) | Every host artifact the byo disclosure names must be removed OR explicitly listed KEPT by `run_uninstall`; the harness greps the parity (token list). New install-time artifact ⇒ add its removal + disclosure line + parity token in the SAME commit. Drive data rule: plain `umount` only, never `-l`/`-f`, never any format op under /mnt/felhom-drives. | diff --git a/scripts/make_og_card.py b/scripts/make_og_card.py new file mode 100644 index 00000000..f0a6e78a --- /dev/null +++ b/scripts/make_og_card.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""make_og_card.py - rebuild the link-preview cards (og:image) around the laptop. + + python scripts/make_og_card.py [--lang hu|en|both] [--check] + +WHAT IT IS FOR. The card Facebook, Messenger, LinkedIn and Slack show when somebody shares +felhom.eu. The 2026-07-18 cards were a headline and two lines of text on a dark field: they showed +the words and none of the product, and they predate the site's current look (operator, 2026-10-12: +"the image is outdated"). This puts the same laptop the home page shows - the frame with the real +screen recording's first frame in it - beside the headline. + +HOW IT KEEPS THE TYPOGRAPHY. The site's font is a woff2 (Plus Jakarta Sans); no font file on this +machine or on DooPlex can be given to a drawing library, and installing one to draw two lines of text +would be a new dependency for a once-a-year picture. So the headline, the logo and the button are +taken as PIXELS from the previous card, which was rendered with the real font: they are cropped, +the headline is scaled to free the right half, and they are composited onto a flat --bg-0 field with +`lighter`, which makes the crop's own background disappear into the new one (both are #0A1220, so +there is no seam to see). Nothing here draws a glyph. + +IF THE WORDS EVER CHANGE this script cannot help - a new headline needs the font, so the card goes +back to the design tool, and this script then takes the new card as its source. + +INPUT website/assets/og-image.png, og-image-en.png (the previous cards - the typography) + website/assets/hero-laptop-frame.webp (the laptop, screen cut to transparency) + website/assets/hero-poster.webp (what the hero's clip opens on) +OUTPUT the same two card files, 1200x630, overwritten. + +--check prints where the ink sits on the current cards and changes nothing. +""" +import argparse +import os +import sys + +from PIL import Image, ImageChops + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +A = os.path.join(ROOT, "website", "assets") + +W, H = 1200, 630 +BG = (10, 18, 32) # --bg-0, which is what sits behind the headline on the old cards +TOPBAR_H = 6 +LEFT = 73 # the logo's own left edge on the old cards +HEAD_TOP = 206 # where the headline's ink starts on the new card +HEAD_WIDTH = 498 # how wide the headline is allowed to be, to leave the laptop its half +LAPTOP_W = 548 +LAPTOP_X = 616 +# The screen hole in hero-laptop-frame.webp, measured off its alpha channel (2026-10-12): +HOLE = (0.11041, 0.12345, 0.79817, 0.64305) + +CARDS = { + # lang: (file, headline band, pill band) - bands are y ranges on the OLD card, generous enough + # to hold accents and descenders + "hu": ("og-image.png", (200, 372), (510, 572)), + "en": ("og-image-en.png", (208, 382), (510, 572)), +} +LOGO_BAND = (76, 132) + + +def ink_bbox(im, y0, y1, thresh=300): + """The bounding box of everything brighter than the background in a horizontal band.""" + px = im.convert("RGB").load() + xs, ys = [], [] + for y in range(y0, y1 + 1): + for x in range(im.size[0]): + if sum(px[x, y]) > thresh: + xs.append(x) + ys.append(y) + if not xs: + sys.exit("no ink in rows %d..%d - the source card is not the one this script expects" % (y0, y1)) + return min(xs), min(ys), max(xs) + 1, max(ys) + 1 + + +def laptop(): + """The frame with the hero's poster frame inside its cut-out screen.""" + frame = Image.open(os.path.join(A, "hero-laptop-frame.webp")).convert("RGBA") + shot = Image.open(os.path.join(A, "hero-poster.webp")).convert("RGB") + fw, fh = frame.size + x, y = int(round(HOLE[0] * fw)), int(round(HOLE[1] * fh)) + w, h = int(round(HOLE[2] * fw)), int(round(HOLE[3] * fh)) + out = Image.new("RGBA", frame.size, (0, 0, 0, 0)) + out.paste(shot.resize((w, h), Image.LANCZOS), (x, y)) + out.alpha_composite(frame) + return out + + +def build(lang, check): + src_name, head_band, pill_band = CARDS[lang] + src_path = os.path.join(A, src_name) + src = Image.open(src_path).convert("RGB") + if src.size != (W, H): + sys.exit("%s is %dx%d, expected %dx%d" % (src_name, src.size[0], src.size[1], W, H)) + + logo = ink_bbox(src, *LOGO_BAND) + head = ink_bbox(src, *head_band) + pill = ink_bbox(src, *pill_band) + if check: + print("%s logo %s headline %s button %s" % (src_name, logo, head, pill)) + return + + card = Image.new("RGB", (W, H), BG) + card.paste(src.crop((0, 0, W, TOPBAR_H)), (0, 0)) # the blue rule along the top + + lap = laptop() + lh = int(round(LAPTOP_W * lap.size[1] / float(lap.size[0]))) + card.paste(lap.resize((LAPTOP_W, lh), Image.LANCZOS), (LAPTOP_X, (H - lh) // 2), + lap.resize((LAPTOP_W, lh), Image.LANCZOS)) + + # the three text blocks, lifted as pixels and laid down with `lighter` so their own + # background (the same #0A1220) vanishes instead of leaving a rectangle + def lay(box, at, scale=1.0): + part = src.crop(box) + if scale != 1.0: + part = part.resize((max(1, int(round(part.size[0] * scale))), + max(1, int(round(part.size[1] * scale)))), Image.LANCZOS) + region = card.crop((at[0], at[1], at[0] + part.size[0], at[1] + part.size[1])) + card.paste(ImageChops.lighter(region, part), at) + + lay(logo, (LEFT, logo[1])) + scale = HEAD_WIDTH / float(head[2] - head[0]) + lay(head, (LEFT, HEAD_TOP), scale) + lay(pill, (LEFT - 1, pill[1])) + + card.save(src_path, "PNG", optimize=True) + print("%-16s %4dx%d %6.1f KB (headline scaled %.2f, laptop %dpx)" + % (src_name, W, H, os.path.getsize(src_path) / 1024.0, scale, LAPTOP_W)) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--lang", default="both", choices=["hu", "en", "both"]) + ap.add_argument("--check", action="store_true", help="print the measured bands, change nothing") + a = ap.parse_args() + for lang in (["hu", "en"] if a.lang == "both" else [a.lang]): + build(lang, a.check) + if not a.check: + print("Bump the ?v= on the og:image / twitter:image URLs, then re-scrape the page in " + "Facebook's Sharing Debugger - a card already attached to a post does not change.") + + +if __name__ == "__main__": + main() diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 9f604a90..b22f10bd 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -1,4 +1,26 @@ -## 2026-10-12 (review round) — the reviewer's findings, fixed +## 2026-10-12 (night) — the share card shows the product + +The operator went to post the link and found the card out of date (2026-10-12): it was the +2026-07-18 design — a headline and two lines of text on a dark field, nothing of the product, and +older than everything the site has become since. + +- **`og-image.png` and `og-image-en.png` rebuilt around the laptop**, with the hero's own poster + frame on its screen: what somebody sees shared in a feed is now the thing itself. +- **The typography is the old card's, to the pixel.** The site's font is a woff2 and no font file + here can be handed to a drawing library, so `scripts/make_og_card.py` lifts the headline, the logo + and the button out of the previous card as pixels and composites them with `lighter` onto flat + `--bg-0` — the crop's own background is the same colour, so there is no seam. **Nothing in that + script draws a glyph, so new words cannot be made with it**: a new headline goes back to the + design tool and the script then takes that card as its source. The two lines of lede are gone + (they would have needed the font); the laptop says what they said. +- **`?v=2` on every `og:image` and `twitter:image`** (20 pages). A scraper caches by URL, so a new + picture at the old URL would have reached nobody. **A card already attached to a post does not + change** — that post has to drop the preview and re-add the link, after a re-scrape. +- The English card carries the same Hungarian-language Immich screen as the Hungarian one, because + it is the same recording the hero uses on both pages. Worth a retake if the English page ever + gets its own. + +## 2026-10-12 (review round) — the reviewer's findings, fixed A page-by-page review of the live site (Playwright, 1440 px and 390 px) found six real defects and two editorial questions. The operator answered three questions first; the answers are in diff --git a/website/adatkezeles.html b/website/adatkezeles.html index be8f47d2..a444321b 100644 --- a/website/adatkezeles.html +++ b/website/adatkezeles.html @@ -15,7 +15,7 @@ - + diff --git a/website/alkalmazasok.html b/website/alkalmazasok.html index 6c430ddc..ca09ccd4 100644 --- a/website/alkalmazasok.html +++ b/website/alkalmazasok.html @@ -18,14 +18,14 @@ - + - +