/**
* Blog Post Page - Client-Side Logic
* Handles fetching and displaying individual blog posts with metadata, sharing, and related posts
*/
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;
// 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 ? `
` : `
`}
${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');
});
}
// 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);