// J Renee Studios — Journal post (article) renderer. // Reads the slug from the URL (/journal//), pulls display meta from // window.J_POSTS (defined in journal-sections.jsx) and the article body from // window.J_POST_CONTENT (generated from _data/journal-posts.json at build time). const { useState: jpUseState, useEffect: jpUseEffect, useMemo: jpUseMemo } = React; function jpGetSlug() { const parts = window.location.pathname.split("/").filter(Boolean); const i = parts.indexOf("journal"); if (i >= 0 && parts[i + 1]) return parts[i + 1]; return parts.length ? parts[parts.length - 1] : ""; } // Minimal inline formatter: [text](url) links, **bold**, and *italic*. function jpInline(text) { if (text == null) return null; const out = []; const re = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|\*([^*]+)\*/g; let last = 0; let key = 0; let m; while ((m = re.exec(text)) !== null) { if (m.index > last) out.push(text.slice(last, m.index)); if (m[1] != null) { const href = m[2]; // Only allow safe link schemes; anything else renders as plain text. const safe = /^(\/|#|https?:\/\/|mailto:|tel:)/i.test(href); const external = /^https?:\/\//i.test(href); if (safe) { out.push( {m[1]} , ); } else { out.push(m[1]); } } else if (m[3] != null) { out.push({m[3]}); } else { out.push({m[4]}); } last = re.lastIndex; } if (last < text.length) out.push(text.slice(last)); return out; } function jpReadingTime(body) { const words = (body || []).reduce((n, b) => { if (b.text) return n + b.text.split(/\s+/).length; if (b.items) return n + b.items.join(" ").split(/\s+/).length; return n; }, 0); return Math.max(1, Math.round(words / 200)); } function JPostBlock({ block }) { const pStyle = { fontFamily: "Poppins, sans-serif", fontWeight: 300, fontSize: 17, lineHeight: 1.85, color: J_INK, margin: "0 0 28px", }; switch (block.type) { case "lead": return (

{jpInline(block.text)}

); case "p": return

{jpInline(block.text)}

; case "h2": return (

{block.text}

); case "h3": return (

{block.text}

); case "quote": return (
{block.text}
); case "list": return ( ); case "image": return (
{block.alt {block.caption ? (
{block.caption}
) : null}
); default: return null; } } function JPostMissing() { return (
Journal

This story is coming soon

This journal entry hasn't been published yet. In the meantime, explore the rest of the journal.

Back to the Journal
); } function JPostApp() { const slug = jpUseMemo(() => jpGetSlug(), []); const content = (window.J_POST_CONTENT || {})[slug] || null; // Prefer the rich card entry (J_POSTS) when present, but fall back to the // generated content meta so a post renders from JSON alone. const meta = jpUseMemo( () => (window.J_POSTS || []).find((p) => p.slug === slug) || (content && content.meta) || null, [slug, content], ); const [scrolled, setScrolled] = jpUseState(false); jpUseEffect(() => { const onScroll = () => setScrolled(window.scrollY > 80); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); jpUseEffect(() => { const els = document.querySelectorAll(".reveal"); const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) e.target.classList.add("in"); }); }, { threshold: 0.12 }); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }, [content]); if (!content || !meta) return ; const body = content.body || []; const minutes = jpReadingTime(body); const heroImages = ((content.meta && content.meta.heroImages) || meta.heroImages || []).filter(Boolean); // The post's own cover comes from its JSON entry (content.meta.image) so it can // differ from the card thumbnail (meta.image, from the J_POSTS card entry). const coverImage = (content.meta && content.meta.image) || meta.image || ""; const written = window.J_POST_CONTENT || {}; const related = (window.J_POSTS || []) .filter((p) => p.slug !== slug && written[p.slug]) .sort((a, b) => { const am = a.city === meta.city ? 0 : 1; const bm = b.city === meta.city ? 0 : 1; return am - bm; }) .slice(0, 3); const metaLine = [meta.category, meta.city && meta.city !== "General" ? meta.city : null, meta.date] .filter(Boolean) .join(" · "); return (
{/* Hero */}
Journal {minutes} min read

{meta.title}

{metaLine}
{/* Cover */} {heroImages.length >= 2 ? (
{heroImages.map((src, i) => ( {`${meta.title} ))}
) : coverImage ? (
{meta.title}
) : null} {/* Body */}
{body.map((block, i) => )} {/* Byline */}
Written by
J. Renée Johnson
Back to the Journal
{/* Related */} {related.length ? (
Keep Reading

More from the Journal

{related.map((p) => )}
) : null} {/* Subscribe form hidden until GoHighLevel is connected — re-enable when the webhook is live. */} {/*
*/}
); } Object.assign(window, { JPostApp }); ReactDOM.createRoot(document.getElementById("root")).render();