// blog-post-page.jsx — Single blog post template for Green Integrations. // Rebuilt v1.9.0 to match the Blog Single Post Design & Implementation Guide // (May 2026 edition). Uses live data from /wp-json/claude-pages/v1/posts/{slug}. // // Major changes from v1.8.0 (per the guide): // • Background switches to cream #F4F2ED throughout body // • Sidebar: 284px wide, 44px gap (was 300/56) // • TOC: collapsible chevron toggle, H3 support, IntersectionObserver active tracking // • Inline Service CTA: dark navy (was amber gradient), placed after 3rd

// by splitting content server-side // • Author Bio: vertical centered layout, link to WP author archive // • Share Card: LinkedIn / X / Email with labels and brand SVGs // • Discovery Call CTA: amber-accent headline, bullets, business-name input // (visual only), green "Book a discovery call" button // • Related Service card uses SIDEBAR target (different from inline) // • Related Articles: white bg, cream cards, "View all →" link // • All schema (Article, FAQPage, BreadcrumbList) handled server-side in // includes/blog-schema.php — see that file, not this one. const API_BASE = '/wp-json/claude-pages/v1'; // Category presentation — slug keys MUST stay in sync with blog-endpoints.php // and blog-page.jsx. Each entry: chipBg/chipFg for chip rendering, accent for // gradient placeholders when there's no cover image. const POST_CATS = { 'power-solar-storage': { label: 'Power · Solar & Storage', slug: 'power-solar-storage', chipBg: '#E8F2EC', chipFg: '#1E4D2B', accent: '#1E4D2B', }, 'transform-ev-electrification': { label: 'Transform · EV & Electrification', slug: 'transform-ev-electrification', chipBg: '#E4EBF6', chipFg: '#1A3562', accent: '#1A3562', }, 'policy-compliance': { label: 'Policy & Compliance', slug: 'policy-compliance', chipBg: '#F7EDDA', chipFg: '#7A5010', accent: '#7A5010', }, 'industry-focus': { label: 'Industry Focus', slug: 'industry-focus', chipBg: '#E4EEF5', chipFg: '#1A4060', accent: '#1A4060', }, 'market-technology': { label: 'Market & Technology', slug: 'market-technology', chipBg: '#F0EBF7', chipFg: '#4A2D6B', accent: '#4A2D6B', }, }; // Resolve the slug from the URL: /resources/insights/{slug}/ const getSlugFromUrl = () => { const parts = window.location.pathname.split('/').filter(Boolean); if (parts.length >= 3 && parts[0] === 'resources' && parts[1] === 'insights') return parts[2]; return parts[parts.length - 1] || ''; }; // Initials helper for the author avatar fallback const initialsFromName = (name) => { if (!name) return '—'; const parts = name.trim().split(/\s+/); if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); }; // Brand SVG icons (Lucide doesn't ship LinkedIn or X) const BrandIcon = ({ name, size = 14, color = 'currentColor' }) => { if (name === 'linkedin') { return ( ); } if (name === 'x' || name === 'twitter') { return ( ); } return null; }; // React Context for the post — every component reads from here const PostContext = React.createContext(null); const usePost = () => React.useContext(PostContext); // ============================================================ // 3.1 — Reading progress bar (fixed top, amber fill, 3px) // ============================================================ const ReadingProgressBar = () => { const [pct, setPct] = React.useState(0); React.useEffect(() => { const onScroll = () => { const h = document.documentElement; const total = h.scrollHeight - h.clientHeight; const p = total > 0 ? Math.min(100, Math.max(0, (h.scrollTop / total) * 100)) : 0; setPct(p); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); }; }, []); return (

); }; // ============================================================ // 3.2 — Breadcrumb: Insights › Category › Title(52) // ============================================================ const Breadcrumb = () => { const post = usePost(); if (!post) return null; const cat = post.category && POST_CATS[post.category.slug]; const title = post.title.length > 52 ? post.title.slice(0, 52).trim() + '…' : post.title; const sep = ; return (
Insights {cat && (<> {sep} {cat.label} )} {sep} {title}
); }; // ============================================================ // 3.3 — Article header (white panel, two-column) // ============================================================ const ArticleHeader = () => { const post = usePost(); if (!post) return null; const cat = post.category && POST_CATS[post.category.slug]; const authorName = post.author ? post.author.name : ''; const initials = initialsFromName(authorName); const handleShare = (kind) => { const url = window.location.href; const text = post.title; if (kind === 'copy') { if (navigator.clipboard) navigator.clipboard.writeText(url); return; } let shareUrl = '#'; if (kind === 'linkedin') shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`; if (kind === 'x') shareUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`; window.open(shareUrl, '_blank', 'noopener,noreferrer'); }; return (
{/* LEFT column */}
{/* Meta row: category chip + reading time */}
{cat && ( {cat.label} )} {post.read_minutes} min read

{post.title}

{post.excerpt && (

{post.excerpt}

)}
{/* RIGHT column — cover image (hidden on mobile via CSS) */}
{post.featured_image && ( {post.featured_image_alt )}
); }; // ============================================================ // 3.5 — Table of Contents (collapsible, gray bg, H3 support, IntersectionObserver) // ============================================================ const TableOfContents = () => { const post = usePost(); const items = (post && post.toc) || []; const [open, setOpen] = React.useState(true); const [active, setActive] = React.useState(items[0] ? items[0].anchor : null); // Set default open state per viewport (open on desktop, collapsed on mobile) React.useEffect(() => { if (typeof window !== 'undefined' && window.innerWidth < 720) { setOpen(false); } }, []); // IntersectionObserver for active section tracking (per spec §3.5) React.useEffect(() => { if (items.length === 0) return; const elements = items .map((it) => document.getElementById(it.anchor)) .filter(Boolean); if (elements.length === 0) return; const obs = new IntersectionObserver((entries) => { // Find the topmost intersecting entry const visible = entries .filter((e) => e.isIntersecting) .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); if (visible[0]) { setActive(visible[0].target.id); } }, { rootMargin: '-8% 0% -72% 0%', threshold: [0] }); elements.forEach((el) => obs.observe(el)); return () => obs.disconnect(); }, [items.length]); if (items.length === 0) return null; return ( ); }; // ============================================================ // 3.7 — Inline Service CTA (dark navy, after 3rd

) // ============================================================ const InlineServiceCta = () => { const post = usePost(); const cta = post && post.service_cta && post.service_cta.inline; if (!cta) return null; return (

); }; // ============================================================ // 3.6 — Article prose body (live HTML from WP, split at 3rd

) // // GI Link Audit (Fix 8): legacy posts may contain external URLs that have // since 404'd or timed out. Known case (May 2026 crawl): // /resources/insights/how-businesses-in-ontario-benefit-from-energy-storage/ // → https://www.oeb.ca/newsroom/2019/all-electricity-pricing-pilots-now-underway // Fix these by editing the post in WP Admin → Posts; this template renders // post_content as-is and cannot rewrite outbound links. // ============================================================ const ArticleProse = () => { const post = usePost(); if (!post) return null; // If server-side split present, render before / CTA / after. // Otherwise fall back to the full content with CTA above. if (post.content_before_cta !== undefined) { return ( <>

{post.content_after_cta && (
)} ); } return ( <>
); }; // ============================================================ // 3.8 — Tags section ("Filed under" label, all tags) // ============================================================ const TagsSection = () => { const post = usePost(); const tags = (post && post.tags) || []; if (tags.length === 0) return null; return (
Filed under {tags.map((t) => ( {t.label} ))}
); }; // ============================================================ // 3.9 — FAQ accordion (first item open by default, single-open pattern) // ============================================================ const FaqSection = () => { const post = usePost(); const faqs = (post && post.faqs) || []; // First item open by default per spec §3.9 const [openIdx, setOpenIdx] = React.useState(0); if (faqs.length === 0) return null; return (

Frequently asked questions

{faqs.map((f, i) => { const open = openIdx === i; return (
{f.answer}
); })}
); }; // ============================================================ // Sidebar primitives // ============================================================ const SidebarCard = ({ children, dark = false, amber = false, style }) => (
{children}
); const SidebarLabel = ({ children, color = '#6B7280', tracking = '0.14em' }) => (
{children}
); // ============================================================ // 4.1 — Author Bio Card (centered avatar/name, left-aligned bio) // ============================================================ const AuthorBioCard = () => { const post = usePost(); const a = post && post.author; if (!a) return null; return ( About the author {/* Centered avatar */}
{a.photo ? {a.photo_alt : initialsFromName(a.name)}
{a.name || 'Green Integrations'}
{a.role &&
{a.role}
}
{a.bio && (

{a.bio}

)} {/* GI Link Audit (Fix 9): the "All articles by {author} →" link previously pointed at /author/{slug}/, a WordPress-auto-generated archive that 404s on this site ("All articles by root →" was the visible artifact). Link removed entirely; author bio is the only thing rendered here now. */}
); }; // ============================================================ // 4.2 — Share Card (LinkedIn / X / Email, with labels) // ============================================================ const ShareCard = () => { const post = usePost(); if (!post) return null; const handleShare = (kind) => { const url = window.location.href; const text = post.title; let shareUrl = '#'; if (kind === 'linkedin') shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`; if (kind === 'x') shareUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`; if (kind === 'email') shareUrl = `mailto:?subject=${encodeURIComponent(text)}&body=${encodeURIComponent(url)}`; if (kind === 'email') { window.location.href = shareUrl; } else { window.open(shareUrl, '_blank', 'noopener,noreferrer'); } }; const buttons = [ { kind: 'linkedin', label: 'LinkedIn', icon: 'linkedin' }, { kind: 'x', label: 'X', icon: 'x' }, { kind: 'email', label: 'Email', icon: 'mail' }, ]; return ( Share this article
{buttons.map((b) => ( ))}
); }; // ============================================================ // 4.3 — Discovery Call CTA card (per spec §4.3, full rebuild) // ============================================================ const DiscoveryCallCard = () => ( Start a conversation {/* Headline with amber accent on "stands on energy costs." */}

Find out where your facility{' '} stands on energy costs.

Schedule a discovery call. No commitment required.

{/* Bullets */}
    {[ "Your current electricity spend and where it's going", 'Which incentive programs apply to your operation', 'Whether onsite generation makes financial sense', ].map((b, i) => (
  • {b}
  • ))}
{/* Business name input (decorative — submission goes to /about/contact/) */} { e.target.style.borderColor = 'rgba(255,255,255,0.35)'; }} onBlur={(e) => { e.target.style.borderColor = 'rgba(255,255,255,0.15)'; }} /> Book a discovery call
Typically replied to within one business day
); // ============================================================ // 4.4 — Related Service Card (amber, sidebar target — DIFFERENT from inline) // ============================================================ const RelatedServiceCard = () => { const post = usePost(); const cta = post && post.service_cta && post.service_cta.sidebar; if (!cta) return null; return ( Also relevant
{cta.label}

{cta.summary}

Explore {cta.label}
); }; // ============================================================ // 5 — Related articles section (white bg, cream cards, "View all →") // ============================================================ const RelatedCard = ({ a }) => { const cat = a.category && POST_CATS[a.category.slug]; const href = a.permalink || ('/resources/insights/' + a.slug + '/'); return (
{ e.currentTarget.style.transform = 'translate3d(0, -2px, 0)'; e.currentTarget.style.boxShadow = '0 18px 38px -22px rgba(15,33,51,0.18)'; e.currentTarget.style.borderColor = 'rgba(15,33,51,0.10)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.borderColor = '#E5E2DA'; }}>
{a.featured_image && ( {a.featured_image_alt )} {cat && ( {cat.label} )}

{a.title}

{a.date_formatted} Read
); }; // ============================================================ // 5.x — Contact CTA (full-width dark navy band) // Rendered after Related Articles, just above the site footer. // Strict-optional: only renders when post.contact_cta is non-null // (REST returns null unless all four ACF fields are filled). // ============================================================ const ContactCtaSection = () => { const post = usePost(); const cta = post && post.contact_cta; if (!cta || !cta.title || !cta.paragraph || !cta.button || !cta.button.label || !cta.button.url) { return null; } return (

{cta.title}

{cta.paragraph}

{cta.button.label}
); }; const RelatedArticles = () => { const post = usePost(); const related = (post && post.related) || []; if (related.length === 0) return null; const cat = post.category && POST_CATS[post.category.slug]; return (

More from {cat ? cat.label : 'Insights'}

View all articles
{related.map((a) => )}
); }; // ============================================================ // Main BlogPostPage — fetch + provider + layout // ============================================================ const BlogPostPage = () => { const [post, setPost] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { const slug = getSlugFromUrl(); if (!slug) { setError('No post slug in URL'); setLoading(false); return; } fetch(`${API_BASE}/posts/${encodeURIComponent(slug)}`) .then((r) => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))) .then((json) => { setPost(json); setError(null); }) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); }, []); // Re-render Lucide icons after each content change React.useEffect(() => { if (window.lucide && typeof window.lucide.createIcons === 'function') { window.lucide.createIcons(); } }, [post, loading]); if (loading) { return (
Loading…
); } if (error || !post) { return (
Article not found
The article you are looking for is not available.
Back to Insights
); } return (
{/* Two-column body — white bg, sticky sidebar */}
{/* Content column */}
{/* Sticky sidebar — 284px */}
); };