Initial Roux Hugo site — fashion journal for roux.pivoine.art
100-post fashion journal generated from ~/projects/ginger content: - Hugo Extended static site with TailwindCSS v4 - WebP image pipeline (thumb/card/og/full sizes via Hugo image processing) - Full SEO: sitemap (501 URLs), OpenGraph with per-post images, Twitter cards - Async page transitions via View Transitions API - Deep-linked URLs: /posts/[slug]/, /categories/[cat]/, /tags/[tag]/, /issues/ - Lightbox with keyboard/swipe nav, thumbnail strip, inverted search index - Issues archive with quarterly release structure - Multi-stage Dockerfile (Tailwind → Hugo → nginx:alpine) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Roux">
|
||||
<title>Roux — peony mark</title>
|
||||
<defs>
|
||||
<radialGradient id="rxBloom" cx="50%" cy="46%" r="58%">
|
||||
<stop offset="0" stop-color="#b34a30"></stop>
|
||||
<stop offset=".6" stop-color="#8a3322"></stop>
|
||||
<stop offset="1" stop-color="#5a1d15"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
|
||||
<g fill="#6e2519" transform="translate(32 32)">
|
||||
<ellipse cx="0" cy="-16" rx="10.5" ry="14"></ellipse>
|
||||
<ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(60)"></ellipse>
|
||||
<ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(120)"></ellipse>
|
||||
<ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(180)"></ellipse>
|
||||
<ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(240)"></ellipse>
|
||||
<ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(300)"></ellipse>
|
||||
</g>
|
||||
|
||||
|
||||
<g fill="url(#rxBloom)" transform="translate(32 32) rotate(30)">
|
||||
<ellipse cx="0" cy="-11" rx="8" ry="11"></ellipse>
|
||||
<ellipse cx="0" cy="-11" rx="8" ry="11" transform="rotate(60)"></ellipse>
|
||||
<ellipse cx="0" cy="-11" rx="8" ry="11" transform="rotate(120)"></ellipse>
|
||||
<ellipse cx="0" cy="-11" rx="8" ry="11" transform="rotate(180)"></ellipse>
|
||||
<ellipse cx="0" cy="-11" rx="8" ry="11" transform="rotate(240)"></ellipse>
|
||||
<ellipse cx="0" cy="-11" rx="8" ry="11" transform="rotate(300)"></ellipse>
|
||||
</g>
|
||||
|
||||
|
||||
<g fill="#9a3a26" transform="translate(32 32) rotate(0)">
|
||||
<ellipse cx="0" cy="-6" rx="5" ry="7.5"></ellipse>
|
||||
<ellipse cx="0" cy="-6" rx="5" ry="7.5" transform="rotate(72)"></ellipse>
|
||||
<ellipse cx="0" cy="-6" rx="5" ry="7.5" transform="rotate(144)"></ellipse>
|
||||
<ellipse cx="0" cy="-6" rx="5" ry="7.5" transform="rotate(216)"></ellipse>
|
||||
<ellipse cx="0" cy="-6" rx="5" ry="7.5" transform="rotate(288)"></ellipse>
|
||||
</g>
|
||||
|
||||
|
||||
<circle cx="32" cy="32" r="3.2" fill="#3a120c"></circle>
|
||||
<circle cx="32" cy="31.4" r="1.2" fill="#c0573c" opacity=".7"></circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,452 @@
|
||||
/* ROUX — app.js */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── Data (injected by Hugo into a JSON script tag)
|
||||
let POSTS = [];
|
||||
try {
|
||||
const el = document.getElementById('roux-data');
|
||||
if (el) POSTS = JSON.parse(el.textContent) || [];
|
||||
} catch (e) { console.warn('roux-data parse failed', e); }
|
||||
|
||||
const STOP = new Set(['the','a','an','of','and','in','on','at','by','with','is','to','for','from','as','into','onto','its','it','that','this','but','or','be','not','no','one','two','three']);
|
||||
|
||||
// ── Helpers
|
||||
function esc(s) { return String(s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
|
||||
function highlight(text, terms) {
|
||||
if (!terms.length) return esc(text);
|
||||
const re = new RegExp('(' + terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')', 'ig');
|
||||
return esc(text).replace(re, '<mark class="hl">$1</mark>');
|
||||
}
|
||||
|
||||
// ── Inverted search index
|
||||
const INDEX = (() => {
|
||||
const map = new Map();
|
||||
function add(token, idx, weight) {
|
||||
const t = token.toLowerCase();
|
||||
if (!t || t.length < 2 || STOP.has(t)) return;
|
||||
let b = map.get(t);
|
||||
if (!b) { b = new Map(); map.set(t, b); }
|
||||
b.set(idx, (b.get(idx) || 0) + weight);
|
||||
}
|
||||
function tok(s) { return (s || '').toLowerCase().split(/[^a-z0-9àâäéèêëîïôöùûüçœæñ]+/i).filter(Boolean); }
|
||||
|
||||
POSTS.forEach((p, i) => {
|
||||
tok(p.title).forEach(t => add(t, i, 6));
|
||||
(p.categories || []).forEach(c => tok(c).forEach(t => add(t, i, 4)));
|
||||
(p.tags || []).forEach(tag => tok(tag).forEach(t => add(t, i, 5)));
|
||||
tok(p.description).forEach(t => add(t, i, 1));
|
||||
tok(p.id).forEach(t => add(t, i, 8));
|
||||
});
|
||||
|
||||
return function search(q) {
|
||||
const tokens = tok(q);
|
||||
if (!tokens.length) return null;
|
||||
const perToken = tokens.map(tok => {
|
||||
const merged = new Map();
|
||||
for (const [term, b] of map) {
|
||||
if (term.startsWith(tok)) {
|
||||
const factor = term === tok ? 1.0 : tok.length / term.length;
|
||||
for (const [pi, w] of b) merged.set(pi, (merged.get(pi) || 0) + w * factor);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
});
|
||||
const first = perToken[0];
|
||||
const scores = new Map();
|
||||
for (const [idx, w] of first) {
|
||||
let total = w, ok = true;
|
||||
for (let i = 1; i < perToken.length; i++) {
|
||||
const v = perToken[i].get(idx);
|
||||
if (!v) { ok = false; break; }
|
||||
total += v;
|
||||
}
|
||||
if (ok) scores.set(idx, total);
|
||||
}
|
||||
return Array.from(scores.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([i]) => POSTS[i]);
|
||||
};
|
||||
})();
|
||||
|
||||
// ── Masthead date
|
||||
const mhDate = document.getElementById('mhDate');
|
||||
if (mhDate) {
|
||||
const d = new Date();
|
||||
mhDate.textContent = d.toLocaleDateString('en-GB', { day:'numeric', month:'long', year:'numeric' });
|
||||
}
|
||||
|
||||
// ── Count display
|
||||
const countEl = document.getElementById('count');
|
||||
function setCount(n) {
|
||||
if (!countEl) return;
|
||||
countEl.innerHTML = `<b>${String(n).padStart(3,'0')}</b> ${n === 1 ? 'post' : 'posts'}`;
|
||||
}
|
||||
setCount(POSTS.length);
|
||||
|
||||
// ── Tabs: sync active state with current URL
|
||||
function syncTabs() {
|
||||
const tabs = document.querySelectorAll('.tabs button[data-cat]');
|
||||
const path = location.pathname;
|
||||
tabs.forEach(btn => {
|
||||
const cat = btn.dataset.cat;
|
||||
const active = (cat === 'All' && (path === '/' || path === '/posts/' || path.startsWith('/posts')))
|
||||
|| (cat !== 'All' && path.toLowerCase().includes(cat.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')));
|
||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
syncTabs();
|
||||
|
||||
// Tab click → navigate to category page
|
||||
document.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.tabs button[data-cat]');
|
||||
if (!btn) return;
|
||||
const cat = btn.dataset.cat;
|
||||
if (cat === 'All') navigate('/');
|
||||
else navigate('/categories/' + cat.toLowerCase().replace(/[\s,]+/g, '-').replace(/[^a-z0-9-]/g, '') + '/');
|
||||
});
|
||||
|
||||
// ── Search popup
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchPop = document.getElementById('searchpop');
|
||||
|
||||
function allCats() {
|
||||
const s = new Set();
|
||||
POSTS.forEach(p => (p.categories || []).forEach(c => s.add(c)));
|
||||
return Array.from(s).slice(0, 8);
|
||||
}
|
||||
function allTags() {
|
||||
const freq = new Map();
|
||||
POSTS.forEach(p => (p.tags || []).forEach(t => freq.set(t, (freq.get(t)||0)+1)));
|
||||
return Array.from(freq.entries()).sort((a,b)=>b[1]-a[1]).slice(0,10).map(([t])=>t);
|
||||
}
|
||||
|
||||
function renderSearchPop(q) {
|
||||
if (!searchPop) return;
|
||||
if (!q) {
|
||||
const cats = allCats();
|
||||
const tags = allTags();
|
||||
searchPop.innerHTML = `
|
||||
<div class="searchpop__section">
|
||||
<div class="searchpop__label">Categories <small>${cats.length}</small></div>
|
||||
<div class="searchpop__chips">
|
||||
${cats.map(c => `<button class="searchpop__chip" data-jump="/categories/${encodeURIComponent(c.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> ${esc(c)}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchpop__section">
|
||||
<div class="searchpop__label">Popular tags <small>${tags.length}</small></div>
|
||||
<div class="searchpop__chips">
|
||||
${tags.map(t => `<button class="searchpop__chip" data-jump="/tags/${encodeURIComponent(t.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> # ${esc(t)}</button>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
searchPop.dataset.open = 'true';
|
||||
return;
|
||||
}
|
||||
const hits = INDEX(q) || [];
|
||||
const terms = q.toLowerCase().split(/\s+/).filter(t => t.length > 1);
|
||||
if (!hits.length) {
|
||||
searchPop.innerHTML = `<div class="searchpop__section"><p style="color:var(--ink-soft);font-size:13px;font-style:italic">No plates match — try <em>gothic</em>, <em>warrior</em>, or <em>neon</em>.</p></div>`;
|
||||
searchPop.dataset.open = 'true';
|
||||
return;
|
||||
}
|
||||
const shown = hits.slice(0, 6);
|
||||
searchPop.innerHTML = `
|
||||
<div class="searchpop__section">
|
||||
<div class="searchpop__label">Plates <small>${hits.length}</small></div>
|
||||
<div class="searchpop__hits">
|
||||
${shown.map(p => `
|
||||
<button class="searchpop__hit" data-jump="${esc(p.url)}">
|
||||
${p.thumb ? `<img src="${esc(p.thumb)}" alt="" loading="lazy" />` : '<div style="width:44px;height:66px;background:var(--paper-2)"></div>'}
|
||||
<div>
|
||||
<div class="t">${highlight(p.title, terms)}</div>
|
||||
<div class="s">${highlight((p.categories||[]).join(', '), terms)}</div>
|
||||
</div>
|
||||
<div class="n">${esc(p.id)}</div>
|
||||
</button>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
searchPop.dataset.open = 'true';
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', () => renderSearchPop(searchInput.value.trim()));
|
||||
searchInput.addEventListener('focus', () => renderSearchPop(searchInput.value.trim()));
|
||||
document.addEventListener('keydown', e => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); searchInput.focus(); renderSearchPop(''); }
|
||||
if (e.key === 'Escape') closeSearch();
|
||||
});
|
||||
}
|
||||
|
||||
if (searchPop) {
|
||||
searchPop.addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-jump]');
|
||||
if (btn) { closeSearch(); navigate(btn.dataset.jump); }
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (searchPop && searchPop.dataset.open === 'true' && !e.target.closest('.subhead')) closeSearch();
|
||||
});
|
||||
|
||||
function closeSearch() {
|
||||
if (searchPop) { searchPop.dataset.open = 'false'; searchPop.innerHTML = ''; }
|
||||
if (searchInput) searchInput.blur();
|
||||
}
|
||||
|
||||
// ── Lightbox
|
||||
const lb = document.getElementById('lb');
|
||||
const lbTrack = document.getElementById('lbTrack');
|
||||
const lbMeta = document.getElementById('lbMeta');
|
||||
const lbThumbs = document.getElementById('lbThumbs');
|
||||
const lbIndex = document.getElementById('lbIndex');
|
||||
|
||||
let lbList = []; // current scoped post list
|
||||
let lbIdx = -1; // index into lbList
|
||||
let lbBuilt = false;
|
||||
|
||||
function lbOpen(slug, scopedList) {
|
||||
lbList = scopedList || POSTS;
|
||||
const idx = lbList.findIndex(p => p.slug === slug);
|
||||
if (idx === -1) return;
|
||||
lbIdx = idx;
|
||||
if (!lbBuilt) buildLbSlides();
|
||||
lb.dataset.open = 'true';
|
||||
document.body.style.overflow = 'hidden';
|
||||
goToSlide(lbIdx, false);
|
||||
lbBuildMeta(lbList[lbIdx]);
|
||||
lbBuildThumbs();
|
||||
}
|
||||
|
||||
function lbClose() {
|
||||
if (!lb) return;
|
||||
lb.dataset.open = 'false';
|
||||
document.body.style.overflow = '';
|
||||
// pop back to gallery if we're on a single-post URL
|
||||
if (window.__ROUX_OPEN_SLUG) {
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
|
||||
function buildLbSlides() {
|
||||
if (!lbTrack) return;
|
||||
lbTrack.innerHTML = lbList.map((p, i) => {
|
||||
const imgSrc = p.card || p.thumb || '';
|
||||
return `<div class="lb__slide" data-i="${i}">
|
||||
<div class="lb__frame">
|
||||
<img class="lb__img" src="${esc(imgSrc)}" alt="${esc(p.title)}" loading="lazy" />
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
lbBuilt = true;
|
||||
// Preload neighbors
|
||||
preloadNeighbors(lbIdx);
|
||||
}
|
||||
|
||||
function goToSlide(idx, smooth = true) {
|
||||
if (!lbTrack) return;
|
||||
lbIdx = Math.max(0, Math.min(idx, lbList.length - 1));
|
||||
lbTrack.style.transition = smooth ? '' : 'none';
|
||||
lbTrack.style.transform = `translateX(-${lbIdx * 100}%)`;
|
||||
if (!smooth) lbTrack.getBoundingClientRect(); // force reflow
|
||||
if (lbIndex) {
|
||||
lbIndex.innerHTML = `<b>${String(lbIdx + 1).padStart(3,'0')}</b> / ${lbList.length}`;
|
||||
}
|
||||
lbBuildMeta(lbList[lbIdx]);
|
||||
syncThumbs();
|
||||
preloadNeighbors(lbIdx);
|
||||
}
|
||||
|
||||
function preloadNeighbors(idx) {
|
||||
if (!lbTrack) return;
|
||||
[-1, 0, 1, 2].forEach(d => {
|
||||
const ni = idx + d;
|
||||
if (ni < 0 || ni >= lbList.length) return;
|
||||
const slide = lbTrack.querySelector(`.lb__slide[data-i="${ni}"]`);
|
||||
if (!slide) return;
|
||||
const img = slide.querySelector('img');
|
||||
if (img && !img.src) img.src = lbList[ni].card || lbList[ni].thumb || '';
|
||||
});
|
||||
}
|
||||
|
||||
function lbBuildMeta(p) {
|
||||
if (!lbMeta || !p) return;
|
||||
const cats = (p.categories || []).join(', ');
|
||||
const tags = (p.tags || []).slice(0, 8);
|
||||
lbMeta.innerHTML = `
|
||||
<div class="lb__cat">${esc(cats)}</div>
|
||||
<h2 class="lb__title">${esc(p.title)}</h2>
|
||||
<p class="lb__desc">${esc(p.description)}</p>
|
||||
<dl class="lb__factgrid">
|
||||
<div class="lb__fact"><dt>Plate</dt><dd>№ ${esc(p.id)}</dd></div>
|
||||
<div class="lb__fact"><dt>Category</dt><dd>${esc((p.categories||['—'])[0])}</dd></div>
|
||||
</dl>
|
||||
<div class="lb__tags">${tags.map(t => `<span class="lb__tag">${esc(t)}</span>`).join('')}</div>
|
||||
<div class="lb__share">
|
||||
<button class="lb__sh lb__sh--primary" id="lbCopy" data-url="${esc(p.url)}">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 1 0-7.07-7.07l-1 1"/><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 1 0 7.07 7.07l1-1"/></svg>
|
||||
<span class="lb__sh-l">Copy link</span>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
document.getElementById('lbCopy')?.addEventListener('click', function () {
|
||||
const url = new URL(this.dataset.url, location.origin).href;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
this.classList.add('is-ok');
|
||||
this.querySelector('.lb__sh-l').textContent = 'Copied!';
|
||||
setTimeout(() => { this.classList.remove('is-ok'); this.querySelector('.lb__sh-l').textContent = 'Copy link'; }, 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function lbBuildThumbs() {
|
||||
if (!lbThumbs) return;
|
||||
lbThumbs.innerHTML = lbList.map((p, i) =>
|
||||
`<button class="lb__thumb" data-i="${i}" aria-current="${i === lbIdx}" aria-label="${esc(p.title)}">
|
||||
<img src="${esc(p.thumb || p.card || '')}" alt="" loading="lazy" />
|
||||
</button>`
|
||||
).join('');
|
||||
lbThumbs.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.lb__thumb');
|
||||
if (btn) goToSlide(parseInt(btn.dataset.i));
|
||||
});
|
||||
}
|
||||
|
||||
function syncThumbs() {
|
||||
if (!lbThumbs) return;
|
||||
lbThumbs.querySelectorAll('.lb__thumb').forEach((b, i) => {
|
||||
b.setAttribute('aria-current', i === lbIdx ? 'true' : 'false');
|
||||
});
|
||||
const active = lbThumbs.querySelector('.lb__thumb[aria-current="true"]');
|
||||
if (active) active.scrollIntoView({ inline: 'center', behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Lightbox controls
|
||||
if (lb) {
|
||||
lb.addEventListener('click', e => {
|
||||
const act = e.target.closest('[data-act]')?.dataset.act;
|
||||
if (act === 'close') lbClose();
|
||||
else if (act === 'prev') goToSlide(lbIdx - 1);
|
||||
else if (act === 'next') goToSlide(lbIdx + 1);
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (lb.dataset.open !== 'true') return;
|
||||
if (e.key === 'Escape') { lbClose(); }
|
||||
if (e.key === 'ArrowLeft') { e.preventDefault(); goToSlide(lbIdx - 1); }
|
||||
if (e.key === 'ArrowRight') { e.preventDefault(); goToSlide(lbIdx + 1); }
|
||||
});
|
||||
|
||||
// Touch swipe
|
||||
let touchX = null;
|
||||
lb.addEventListener('touchstart', e => { touchX = e.touches[0].clientX; }, { passive: true });
|
||||
lb.addEventListener('touchend', e => {
|
||||
if (touchX === null) return;
|
||||
const dx = e.changedTouches[0].clientX - touchX;
|
||||
if (Math.abs(dx) > 50) { dx < 0 ? goToSlide(lbIdx + 1) : goToSlide(lbIdx - 1); }
|
||||
touchX = null;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Card clicks → open lightbox (no full page navigation for same-origin)
|
||||
document.addEventListener('click', e => {
|
||||
const card = e.target.closest('.card[data-slug]');
|
||||
if (!card) return;
|
||||
e.preventDefault();
|
||||
const slug = card.dataset.slug;
|
||||
const visibleSlugs = Array.from(document.querySelectorAll('.card[data-slug]')).map(c => c.dataset.slug);
|
||||
const scopedList = POSTS.filter(p => visibleSlugs.includes(p.slug));
|
||||
// Update URL without page reload
|
||||
history.pushState({ slug }, '', card.href);
|
||||
lbOpen(slug, scopedList.length ? scopedList : POSTS);
|
||||
});
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', e => {
|
||||
if (lb && lb.dataset.open === 'true') {
|
||||
if (!e.state?.slug) { lb.dataset.open = 'false'; document.body.style.overflow = ''; }
|
||||
}
|
||||
});
|
||||
|
||||
// ── Ribbon
|
||||
const ribbon = document.getElementById('ribbon');
|
||||
const ribbonClose = document.getElementById('ribbonClose');
|
||||
if (ribbonClose && ribbon) {
|
||||
if (sessionStorage.getItem('ribbon-closed')) ribbon.classList.add('hidden');
|
||||
ribbonClose.addEventListener('click', () => {
|
||||
ribbon.classList.add('hidden');
|
||||
sessionStorage.setItem('ribbon-closed', '1');
|
||||
});
|
||||
}
|
||||
|
||||
// ── Async page transitions (View Transitions API)
|
||||
function isSameOrigin(url) {
|
||||
try { return new URL(url).origin === location.origin; } catch { return false; }
|
||||
}
|
||||
|
||||
async function navigate(url) {
|
||||
if (!isSameOrigin(url)) { location.href = url; return; }
|
||||
|
||||
if (!document.startViewTransition) {
|
||||
location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
document.startViewTransition(async () => {
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'fetch' } });
|
||||
const text = await res.text();
|
||||
const doc = new DOMParser().parseFromString(text, 'text/html');
|
||||
|
||||
const newContent = doc.getElementById('content');
|
||||
const newData = doc.getElementById('roux-data');
|
||||
const oldContent = document.getElementById('content');
|
||||
|
||||
if (newContent && oldContent) {
|
||||
oldContent.replaceWith(newContent);
|
||||
}
|
||||
if (newData) {
|
||||
try {
|
||||
POSTS = JSON.parse(newData.textContent) || [];
|
||||
lbBuilt = false; // force rebuild on next open
|
||||
} catch {}
|
||||
}
|
||||
|
||||
document.title = doc.title;
|
||||
history.pushState({}, '', url);
|
||||
|
||||
// Re-init page state
|
||||
setCount(POSTS.length);
|
||||
syncTabs();
|
||||
|
||||
// If new page has a slug to open
|
||||
const script = doc.querySelector('script[data-open-slug]');
|
||||
const openSlug = window.__ROUX_OPEN_SLUG = doc.querySelector('[data-open-slug]')?.dataset.openSlug || null;
|
||||
if (openSlug) {
|
||||
const visibleSlugs = Array.from(document.querySelectorAll('.card[data-slug]')).map(c => c.dataset.slug);
|
||||
const scoped = POSTS.filter(p => visibleSlugs.includes(p.slug));
|
||||
lbOpen(openSlug, scoped.length ? scoped : POSTS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Intercept link clicks for async transitions
|
||||
document.addEventListener('click', e => {
|
||||
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
const a = e.target.closest('a[href]');
|
||||
if (!a || a.target || a.hasAttribute('download')) return;
|
||||
if (!isSameOrigin(a.href)) return;
|
||||
// Skip lightbox card clicks (handled above)
|
||||
if (a.closest('.card[data-slug]')) return;
|
||||
// Skip anchor links
|
||||
if (a.href.includes('#')) return;
|
||||
e.preventDefault();
|
||||
navigate(a.href);
|
||||
});
|
||||
|
||||
// ── On single-post pages: auto-open lightbox
|
||||
if (window.__ROUX_OPEN_SLUG && POSTS.length) {
|
||||
const visibleSlugs = Array.from(document.querySelectorAll('.card[data-slug]')).map(c => c.dataset.slug);
|
||||
const scoped = POSTS.filter(p => visibleSlugs.includes(p.slug));
|
||||
lbOpen(window.__ROUX_OPEN_SLUG, scoped.length ? scoped : POSTS);
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user