/** * Blog Post Page - Client-Side Logic * Handles fetching and displaying individual blog posts with metadata, sharing, and related posts */ /* global initPresentationMode */ let currentPost = null; /** * Initialize the blog post page */ async function init() { try { // Get slug from URL parameter const urlParams = new URLSearchParams(window.location.search); const slug = urlParams.get('slug'); if (!slug) { showError('No blog post specified'); return; } await loadPost(slug); } catch (error) { console.error('Error initializing blog post:', error); showError('Failed to load blog post'); } } /** * Load blog post by slug */ async function loadPost(slug) { try { const response = await fetch(`/api/blog/${slug}`); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Post not found'); } currentPost = data.post; // Check for presentation mode if (typeof initPresentationMode === 'function' && initPresentationMode(currentPost)) { return; // Presentation mode took over } // Render post renderPost(); // Load related posts loadRelatedPosts(); // Attach event listeners attachEventListeners(); } catch (error) { console.error('Error loading post:', error); showError(error.message || 'Post not found'); } } /** * Helper: Safely set element content */ function safeSetContent(elementId, content) { const element = document.getElementById(elementId); if (element) { element.textContent = content; return true; } return false; } /** * Helper: Safely set element attribute */ function safeSetAttribute(elementId, attribute, value) { const element = document.getElementById(elementId); if (element) { element.setAttribute(attribute, value); return true; } return false; } /** * Render the blog post */ function renderPost() { // Hide loading state safeSetClass('loading-state', 'add', 'hidden'); safeSetClass('error-state', 'add', 'hidden'); // Show post content safeSetClass('post-content', 'remove', 'hidden'); // Update page title and meta description safeSetContent('page-title', `${currentPost.title} | Tractatus Blog`); safeSetAttribute('page-description', 'content', currentPost.excerpt || currentPost.title); // Update social media meta tags updateSocialMetaTags(currentPost); // Update breadcrumb safeSetContent('breadcrumb-title', truncate(currentPost.title, 50)); // Render post header if (currentPost.category) { safeSetContent('post-category', currentPost.category); } else { const categoryEl = document.getElementById('post-category'); if (categoryEl) categoryEl.style.display = 'none'; } safeSetContent('post-title', currentPost.title); // Author - handle both flat (author_name) and nested (author.name) structures const authorName = currentPost.author_name || currentPost.author?.name || 'Tractatus Team'; safeSetContent('post-author', authorName); // Date const publishedDate = new Date(currentPost.published_at); const formattedDate = publishedDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); safeSetContent('post-date', formattedDate); safeSetAttribute('post-date', 'datetime', currentPost.published_at); // Read time const wordCount = currentPost.content ? currentPost.content.split(/\s+/).length : 0; const readTime = Math.max(1, Math.ceil(wordCount / 200)); safeSetContent('post-read-time', `${readTime} min read`); // Tags if (currentPost.tags && currentPost.tags.length > 0) { const tagsHTML = currentPost.tags.map(tag => ` ${escapeHtml(tag)} `).join(''); const tagsEl = document.getElementById('post-tags'); if (tagsEl) tagsEl.innerHTML = tagsHTML; safeSetClass('post-tags-container', 'remove', 'hidden'); } // AI disclosure (if AI-assisted) if (currentPost.ai_assisted || currentPost.metadata?.ai_assisted) { safeSetClass('ai-disclosure', 'remove', 'hidden'); } // Post body - render as cards if sections exist, otherwise render as HTML const bodyEl = document.getElementById('post-body'); if (bodyEl) { if (currentPost.sections && currentPost.sections.length > 0) { bodyEl.innerHTML = renderCardSections(currentPost.sections); } else { const bodyHTML = currentPost.content_html || convertMarkdownToHTML(currentPost.content); bodyEl.innerHTML = bodyHTML; } } } /** * Render card-based sections for better UI */ function renderCardSections(sections) { const cardsHTML = sections.map(section => { // Category badge color const categoryColors = { 'critical': 'bg-red-100 text-red-800 border-red-200', 'practical': 'bg-green-100 text-green-800 border-green-200', 'research': 'bg-blue-100 text-blue-800 border-blue-200', 'conceptual': 'bg-purple-100 text-purple-800 border-purple-200' }; // Technical level indicator const levelIcons = { 'beginner': '⭐', 'intermediate': '⭐⭐', 'advanced': '⭐⭐⭐' }; const categoryClass = categoryColors[section.category] || 'bg-gray-100 text-gray-800 border-gray-200'; const levelIcon = levelIcons[section.technicalLevel] || '⭐⭐'; return `
Section ${section.number} ${escapeHtml(section.category.toUpperCase())} ${levelIcon} ${escapeHtml(section.technicalLevel)}

${escapeHtml(section.title)}

${section.readingTime} min
${section.content_html}
`; }).join(''); return `
${cardsHTML}
`; } /** * Helper: Safely add/remove class */ function safeSetClass(elementId, action, className) { const element = document.getElementById(elementId); if (element) { if (action === 'add') { element.classList.add(className); } else if (action === 'remove') { element.classList.remove(className); } return true; } return false; } /** * Load related posts (same category or similar tags) */ async function loadRelatedPosts() { try { // Fetch all published posts const response = await fetch('/api/blog'); const data = await response.json(); if (!data.success) return; let allPosts = data.posts || []; // Filter out current post allPosts = allPosts.filter(post => post._id !== currentPost._id); // Find related posts (same category, or matching tags) let relatedPosts = []; // Priority 1: Same category if (currentPost.category) { relatedPosts = allPosts.filter(post => post.category === currentPost.category); } // Priority 2: Matching tags (if not enough from same category) if (relatedPosts.length < 3 && currentPost.tags && currentPost.tags.length > 0) { const tagMatches = allPosts.filter(post => { if (!post.tags || post.tags.length === 0) return false; return post.tags.some(tag => currentPost.tags.includes(tag)); }); relatedPosts = [...new Set([...relatedPosts, ...tagMatches])]; } // Priority 3: Most recent posts (if still not enough) if (relatedPosts.length < 3) { const recentPosts = allPosts .sort((a, b) => new Date(b.published_at) - new Date(a.published_at)) .slice(0, 3); relatedPosts = [...new Set([...relatedPosts, ...recentPosts])]; } // Limit to 2-3 related posts relatedPosts = relatedPosts.slice(0, 2); if (relatedPosts.length > 0) { renderRelatedPosts(relatedPosts); } } catch (error) { console.error('Error loading related posts:', error); // Silently fail - related posts are not critical } } /** * Render related posts section */ function renderRelatedPosts(posts) { const relatedPostsHTML = posts.map(post => { const publishedDate = new Date(post.published_at); const formattedDate = publishedDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); return `
${post.featured_image ? `
${escapeHtml(post.title)}
` : `
`}
${post.category ? ` ${escapeHtml(post.category)} ` : ''}

${escapeHtml(post.title)}

`; }).join(''); document.getElementById('related-posts').innerHTML = relatedPostsHTML; document.getElementById('related-posts-section').classList.remove('hidden'); } /** * Attach event listeners for sharing and interactions */ function attachEventListeners() { // Share on Twitter const shareTwitterBtn = document.getElementById('share-twitter'); if (shareTwitterBtn) { shareTwitterBtn.addEventListener('click', () => { const url = encodeURIComponent(window.location.href); const text = encodeURIComponent(currentPost.title); window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=550,height=420'); }); } // Share on LinkedIn const shareLinkedInBtn = document.getElementById('share-linkedin'); if (shareLinkedInBtn) { shareLinkedInBtn.addEventListener('click', () => { const url = encodeURIComponent(window.location.href); window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'width=550,height=420'); }); } // Enter presentation mode const presentBtn = document.getElementById('enter-presentation'); if (presentBtn) { presentBtn.addEventListener('click', () => { if (typeof initPresentationMode === 'function') { // Update URL to reflect presentation mode const url = new URL(window.location); url.searchParams.set('mode', 'presentation'); window.history.pushState({}, '', url.toString()); initPresentationMode(currentPost); } }); } // Copy link const copyLinkBtn = document.getElementById('copy-link'); if (copyLinkBtn) { copyLinkBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(window.location.href); // Show temporary success message const originalHTML = copyLinkBtn.innerHTML; copyLinkBtn.innerHTML = ` Copied! `; copyLinkBtn.classList.add('bg-green-600'); copyLinkBtn.classList.remove('bg-gray-600'); setTimeout(() => { copyLinkBtn.innerHTML = originalHTML; copyLinkBtn.classList.remove('bg-green-600'); copyLinkBtn.classList.add('bg-gray-600'); }, 2000); } catch (err) { console.error('Failed to copy link:', err); // Show error in button const originalHTML = copyLinkBtn.innerHTML; copyLinkBtn.innerHTML = ` Failed `; copyLinkBtn.classList.add('bg-red-600'); copyLinkBtn.classList.remove('bg-gray-600'); setTimeout(() => { copyLinkBtn.innerHTML = originalHTML; copyLinkBtn.classList.remove('bg-red-600'); copyLinkBtn.classList.add('bg-gray-600'); }, 2000); } }); } } /** * Show error state */ function showError(message) { document.getElementById('loading-state').classList.add('hidden'); document.getElementById('post-content').classList.add('hidden'); const errorStateEl = document.getElementById('error-state'); errorStateEl.classList.remove('hidden'); const errorMessageEl = document.getElementById('error-message'); if (errorMessageEl) { errorMessageEl.textContent = message; } } /** * Convert markdown to HTML (basic implementation - can be enhanced with a library) */ function convertMarkdownToHTML(markdown) { if (!markdown) return ''; let html = markdown; // Headers html = html.replace(/^### (.*$)/gim, '

$1

'); html = html.replace(/^## (.*$)/gim, '

$1

'); html = html.replace(/^# (.*$)/gim, '

$1

'); // Bold html = html.replace(/\*\*(.*?)\*\*/g, '$1'); // Italic html = html.replace(/\*(.*?)\*/g, '$1'); // Links html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Paragraphs html = html.replace(/\n\n/g, '

'); html = `

${ html }

`; // Line breaks html = html.replace(/\n/g, '
'); return html; } /** * Update social media meta tags for sharing */ function updateSocialMetaTags(post) { const currentUrl = window.location.href; const excerpt = post.excerpt || post.title; const imageUrl = post.featured_image || 'https://agenticgovernance.digital/images/tractatus-icon.svg'; const authorName = post.author_name || post.author?.name || 'Tractatus Team'; // Open Graph tags document.getElementById('og-title').setAttribute('content', post.title); document.getElementById('og-description').setAttribute('content', excerpt); document.getElementById('og-url').setAttribute('content', currentUrl); document.getElementById('og-image').setAttribute('content', imageUrl); if (post.published_at) { document.getElementById('article-published-time').setAttribute('content', post.published_at); } document.getElementById('article-author').setAttribute('content', authorName); // Twitter Card tags document.getElementById('twitter-title').setAttribute('content', post.title); document.getElementById('twitter-description').setAttribute('content', excerpt); document.getElementById('twitter-image').setAttribute('content', imageUrl); document.getElementById('twitter-image-alt').setAttribute('content', `${post.title} - Tractatus AI Safety Framework`); } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Truncate text to specified length */ function truncate(text, maxLength) { if (!text || text.length <= maxLength) return text; return `${text.substring(0, maxLength) }...`; } // Initialize on page load document.addEventListener('DOMContentLoaded', init);