openvibers/afresume Commits

Link previews for in-page anchors: each /#section link gets its own shot scrolled to that section; nav hash links no longer show the home hero

OP OpenVibers committed 20 Sept 2026 · b076129947 · 45 files, +61 −2


Changes

ddiff --git a/public/assets/img/previews/alexfrison.net.jpg b/public/assets/img/previews/alexfrison.net.jpg
iindex b31c56c..9324f99 100644
BBinary files a/public/assets/img/previews/alexfrison.net.jpg and b/public/assets/img/previews/alexfrison.net.jpg differ
ddiff --git a/public/assets/img/previews/local_home.jpg b/public/assets/img/previews/local_home.jpg
iindex 681b74b..4297537 100644
BBinary files a/public/assets/img/previews/local_home.jpg and b/public/assets/img/previews/local_home.jpg differ
ddiff --git a/public/assets/img/previews/local_home__contact.jpg b/public/assets/img/previews/local_home__contact.jpg
nnew file mode 100644
iindex 0000000..d8c76da
BBinary files /dev/null and b/public/assets/img/previews/local_home__contact.jpg differ
ddiff --git a/public/assets/img/previews/local_home__experience.jpg b/public/assets/img/previews/local_home__experience.jpg
nnew file mode 100644
iindex 0000000..33e8325
BBinary files /dev/null and b/public/assets/img/previews/local_home__experience.jpg differ
ddiff --git a/public/assets/img/previews/local_home__metrics.jpg b/public/assets/img/previews/local_home__metrics.jpg
nnew file mode 100644
iindex 0000000..1a96380
BBinary files /dev/null and b/public/assets/img/previews/local_home__metrics.jpg differ
ddiff --git a/public/assets/img/previews/local_home__network.jpg b/public/assets/img/previews/local_home__network.jpg
nnew file mode 100644
iindex 0000000..af4322b
BBinary files /dev/null and b/public/assets/img/previews/local_home__network.jpg differ
ddiff --git a/public/assets/img/previews/local_home__skills.jpg b/public/assets/img/previews/local_home__skills.jpg
nnew file mode 100644
iindex 0000000..e828cf0
BBinary files /dev/null and b/public/assets/img/previews/local_home__skills.jpg differ
ddiff --git a/public/assets/img/previews/local_home__work.jpg b/public/assets/img/previews/local_home__work.jpg
nnew file mode 100644
iindex 0000000..c2592fe
BBinary files /dev/null and b/public/assets/img/previews/local_home__work.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio.jpg b/public/assets/img/previews/local_portfolio.jpg
iindex f9a2bb3..afdc457 100644
BBinary files a/public/assets/img/previews/local_portfolio.jpg and b/public/assets/img/previews/local_portfolio.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg b/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg
iindex 367ef3b..e4c56b1 100644
BBinary files a/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg and b/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg b/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg
iindex 62f2b33..edbe8bf 100644
BBinary files a/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg and b/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_gameserverlists.jpg b/public/assets/img/previews/local_portfolio_gameserverlists.jpg
iindex 61eb54f..6b01724 100644
BBinary files a/public/assets/img/previews/local_portfolio_gameserverlists.jpg and b/public/assets/img/previews/local_portfolio_gameserverlists.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_gameserverstats.jpg b/public/assets/img/previews/local_portfolio_gameserverstats.jpg
iindex fff7186..62feb85 100644
BBinary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and b/public/assets/img/previews/local_portfolio_gameserverstats.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_hartools.jpg b/public/assets/img/previews/local_portfolio_hartools.jpg
iindex ca4a14d..acf6795 100644
BBinary files a/public/assets/img/previews/local_portfolio_hartools.jpg and b/public/assets/img/previews/local_portfolio_hartools.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg b/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg
iindex 9e22083..5974a6c 100644
BBinary files a/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg and b/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg b/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg
iindex eb9a866..452e359 100644
BBinary files a/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg and b/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg b/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg
iindex 5b75f75..b6b2402 100644
BBinary files a/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg and b/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_live-translation.jpg b/public/assets/img/previews/local_portfolio_live-translation.jpg
iindex bc642ce..699d9a3 100644
BBinary files a/public/assets/img/previews/local_portfolio_live-translation.jpg and b/public/assets/img/previews/local_portfolio_live-translation.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg b/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg
iindex 783545d..26a92ea 100644
BBinary files a/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg and b/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_mp-select-mini.jpg b/public/assets/img/previews/local_portfolio_mp-select-mini.jpg
iindex 5988b94..61eef08 100644
BBinary files a/public/assets/img/previews/local_portfolio_mp-select-mini.jpg and b/public/assets/img/previews/local_portfolio_mp-select-mini.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-community.jpg b/public/assets/img/previews/local_portfolio_openvibe-community.jpg
iindex 12442a2..51bd989 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-community.jpg and b/public/assets/img/previews/local_portfolio_openvibe-community.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg b/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg
iindex d5e72b3..35e811e 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg and b/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-games.jpg b/public/assets/img/previews/local_portfolio_openvibe-games.jpg
iindex efc7cc7..6f6ff2a 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-games.jpg and b/public/assets/img/previews/local_portfolio_openvibe-games.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-live.jpg b/public/assets/img/previews/local_portfolio_openvibe-live.jpg
iindex ed8a1fc..1c44142 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-live.jpg and b/public/assets/img/previews/local_portfolio_openvibe-live.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-media.jpg b/public/assets/img/previews/local_portfolio_openvibe-media.jpg
iindex e40b61c..3afe48f 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-media.jpg and b/public/assets/img/previews/local_portfolio_openvibe-media.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-network.jpg b/public/assets/img/previews/local_portfolio_openvibe-network.jpg
iindex b3f03e3..25314b8 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-network.jpg and b/public/assets/img/previews/local_portfolio_openvibe-network.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-sites.jpg b/public/assets/img/previews/local_portfolio_openvibe-sites.jpg
iindex d0afd1d..d2bf4e4 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-sites.jpg and b/public/assets/img/previews/local_portfolio_openvibe-sites.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-tools.jpg b/public/assets/img/previews/local_portfolio_openvibe-tools.jpg
iindex e6bbc2a..7e93250 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-tools.jpg and b/public/assets/img/previews/local_portfolio_openvibe-tools.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_perf-harness.jpg b/public/assets/img/previews/local_portfolio_perf-harness.jpg
iindex fe022ab..ba9b844 100644
BBinary files a/public/assets/img/previews/local_portfolio_perf-harness.jpg and b/public/assets/img/previews/local_portfolio_perf-harness.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg b/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg
iindex 514dbaf..a8dcaa0 100644
BBinary files a/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg and b/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_powerchat-payments.jpg b/public/assets/img/previews/local_portfolio_powerchat-payments.jpg
iindex 1261bc6..4ed0263 100644
BBinary files a/public/assets/img/previews/local_portfolio_powerchat-payments.jpg and b/public/assets/img/previews/local_portfolio_powerchat-payments.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_pytools-workshop.jpg b/public/assets/img/previews/local_portfolio_pytools-workshop.jpg
iindex 747a924..2a90698 100644
BBinary files a/public/assets/img/previews/local_portfolio_pytools-workshop.jpg and b/public/assets/img/previews/local_portfolio_pytools-workshop.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg
iindex 415d2bc..073dd66 100644
BBinary files a/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg and b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg b/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg
iindex 65b8c2f..fcf9b49 100644
BBinary files a/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg and b/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg differ
ddiff --git a/public/assets/img/previews/local_resume.jpg b/public/assets/img/previews/local_resume.jpg
iindex f362476..12ef07d 100644
BBinary files a/public/assets/img/previews/local_resume.jpg and b/public/assets/img/previews/local_resume.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.chat.jpg b/public/assets/img/previews/openvibe.chat.jpg
iindex 8b6f6fd..dc4d1fd 100644
BBinary files a/public/assets/img/previews/openvibe.chat.jpg and b/public/assets/img/previews/openvibe.chat.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.community.jpg b/public/assets/img/previews/openvibe.community.jpg
iindex 18ab4eb..9c6d950 100644
BBinary files a/public/assets/img/previews/openvibe.community.jpg and b/public/assets/img/previews/openvibe.community.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.games.jpg b/public/assets/img/previews/openvibe.games.jpg
iindex 5c3daa9..9684935 100644
BBinary files a/public/assets/img/previews/openvibe.games.jpg and b/public/assets/img/previews/openvibe.games.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.live.jpg b/public/assets/img/previews/openvibe.live.jpg
iindex 76191e8..97c55b1 100644
BBinary files a/public/assets/img/previews/openvibe.live.jpg and b/public/assets/img/previews/openvibe.live.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.media.jpg b/public/assets/img/previews/openvibe.media.jpg
iindex 91c950a..da0d391 100644
BBinary files a/public/assets/img/previews/openvibe.media.jpg and b/public/assets/img/previews/openvibe.media.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.network.jpg b/public/assets/img/previews/openvibe.network.jpg
iindex 17000f0..76599b8 100644
BBinary files a/public/assets/img/previews/openvibe.network.jpg and b/public/assets/img/previews/openvibe.network.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.tools.jpg b/public/assets/img/previews/openvibe.tools.jpg
iindex 12487b7..554a0e5 100644
BBinary files a/public/assets/img/previews/openvibe.tools.jpg and b/public/assets/img/previews/openvibe.tools.jpg differ
ddiff --git a/scripts/make-link-previews.mjs b/scripts/make-link-previews.mjs
iindex bf22bc0..9f0c1b9 100644
---- a/scripts/make-link-previews.mjs
++++ b/scripts/make-link-previews.mjs
@@@ -19,6 +19,21 @@ const external = [
];
const internal = ['/', '/resume/', '/portfolio/', ...readdirSync(`${ROOT}dist/portfolio`).filter((d) => existsSync(`${ROOT}dist/portfolio/${d}/index.html`)).map((d) => `/portfolio/${d}/`)];
++// Every in-page anchor the built site links to (nav items, "See the network", skip links...) gets
++// its own shot, scrolled to that section, so hovering /#experience previews the Experience
++// section rather than the top of the home page.
++const hashTargets = new Map(); // "path#hash" -> { path, hash }
++const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(`${dir}/${e.name}`) : e.name.endsWith('.html') ? [`${dir}/${e.name}`] : []);
++for (const file of walk(`${ROOT}dist`)) {
++ const pagePath = file.replace(`${ROOT}dist`, '').replace(/index\.html$/, '') || '/';
++ const html = readFileSync(file, 'utf8');
++ for (const m of html.matchAll(/href="((?:\/[a-z0-9\-\/]*)?)#([a-z0-9\-]+)"/gi)) {
++ const path = m[1] ? (m[1].endsWith('/') ? m[1] : m[1] + '/') : pagePath;
++ if (m[2] === 'main' || !existsSync(`${ROOT}dist${path}index.html`)) continue; // #main is the skip link
++ hashTargets.set(`${path}#${m[2]}`, { path, hash: m[2] });
++ }
++}
++
const server = spawn('python3', ['-m', 'http.server', '4399'], { cwd: `${ROOT}dist`, stdio: 'ignore' });
await new Promise((r) => setTimeout(r, 800));
@@@ -26,12 +41,22 @@ const browser = await chromium.launch({ executablePath: '/usr/bin/google-chrome'
const ctx = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: SCALE, colorScheme: 'dark',
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36' });
const map = {};
--const shoot = async (url, key, label, file) => {
++const shoot = async (url, key, label, file, hash) => {
const page = await ctx.newPage();
try {
await page.addInitScript(() => { try { localStorage.setItem('ov_theme_asked', '1'); } catch {} });
await page.goto(url, { waitUntil: 'networkidle', timeout: 25000 }).catch(() => {});
await page.waitForTimeout(url.startsWith('http://localhost') ? 2500 : 1500);
++ if (hash) {
++ const found = await page.evaluate((id) => {
++ const el = document.getElementById(id); if (!el) return false;
++ const y = el.getBoundingClientRect().top + scrollY - 84;
++ if (window.__lenis) window.__lenis.scrollTo(y, { immediate: true }); else scrollTo(0, y);
++ return true;
++ }, hash);
++ if (!found) throw new Error('no #' + hash);
++ await page.waitForTimeout(1600); // let reveal animations finish
++ }
await page.screenshot({ path: `${OUT}/${file}.jpg`, type: 'jpeg', quality: 74, clip: { x: 0, y: 0, width: W, height: H } });
map[key] = { img: `/assets/img/previews/${file}.jpg`, label };
console.log('ok', key);
@@@ -46,6 +71,10 @@ for (const p of internal) {
const label = p === '/' ? 'alexfrison.net' : `alexfrison.net${p.replace(/\/$/, '')}`;
await shoot(`http://localhost:4399${p}`, `path:${p.replace(/\/$/, '') || '/'}`, label, 'local' + (p.replace(/\/$/, '') || '/home').replace(/\//g, '_'));
}
++for (const { path, hash } of hashTargets.values()) {
++ const short = path.replace(/\/$/, '') || '/';
++ await shoot(`http://localhost:4399${path}`, `path:${short}#${hash}`, `alexfrison.net${short === '/' ? '' : short}#${hash}`, 'local' + (short === '/' ? '/home' : short).replace(/\//g, '_') + '__' + hash, hash);
++}
await browser.close();
server.kill();
writeFileSync(`${ROOT}src/data/link-previews.json`, JSON.stringify(map, null, 2) + '\n');
ddiff --git a/src/data/link-previews.json b/src/data/link-previews.json
iindex 0dcc9fc..ee40606 100644
---- a/src/data/link-previews.json
++++ b/src/data/link-previews.json
@@@ -150,5 +150,29 @@
"path:/portfolio/youtube-desktop-client": {
"img": "/assets/img/previews/local_portfolio_youtube-desktop-client.jpg",
"label": "alexfrison.net/portfolio/youtube-desktop-client"
++ },
++ "path:/#network": {
++ "img": "/assets/img/previews/local_home__network.jpg",
++ "label": "alexfrison.net#network"
++ },
++ "path:/#work": {
++ "img": "/assets/img/previews/local_home__work.jpg",
++ "label": "alexfrison.net#work"
++ },
++ "path:/#experience": {
++ "img": "/assets/img/previews/local_home__experience.jpg",
++ "label": "alexfrison.net#experience"
++ },
++ "path:/#skills": {
++ "img": "/assets/img/previews/local_home__skills.jpg",
++ "label": "alexfrison.net#skills"
++ },
++ "path:/#contact": {
++ "img": "/assets/img/previews/local_home__contact.jpg",
++ "label": "alexfrison.net#contact"
++ },
++ "path:/#metrics": {
++ "img": "/assets/img/previews/local_home__metrics.jpg",
++ "label": "alexfrison.net#metrics"
}
}
ddiff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
iindex 714a423..a36e7e3 100644
---- a/src/layouts/Base.astro
++++ b/src/layouts/Base.astro
@@@ -383,7 +383,13 @@ const imageUrl = new URL(image, site).toString();
const keyFor = (a: HTMLAnchorElement) => {
try {
const u = new URL(a.href, location.href);
-- if (u.origin === location.origin) { const p = u.pathname.replace(/\/$/, '') || '/'; return previews['path:' + p] ? 'path:' + p : null; }
++ if (u.origin === location.origin) {
++ const p = u.pathname.replace(/\/$/, '') || '/';
++ const h = u.hash.replace(/^#/, '');
++ if (h && previews['path:' + p + '#' + h]) return 'path:' + p + '#' + h;
++ if (h && p === (location.pathname.replace(/\/$/, '') || '/')) return null; // same-page anchor with no shot: nothing useful to show
++ return previews['path:' + p] ? 'path:' + p : null;
++ }
const h = u.host.replace(/^www\./, '');
return previews[h] ? h : (h.includes('linkedin') ? 'linkedin.com' : null);
} catch { return null; }