/**
* 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');
}
}
/**
* Render the blog post
*/
function renderPost() {
// Hide loading state
document.getElementById('loading-state').classList.add('hidden');
document.getElementById('error-state').classList.add('hidden');
// Show post content
const postContentEl = document.getElementById('post-content');
postContentEl.classList.remove('hidden');
// Update page title and meta description
document.getElementById('page-title').textContent = `${currentPost.title} | Tractatus Blog`;
document.getElementById('page-description').setAttribute('content', currentPost.excerpt || currentPost.title);
// Update social media meta tags
updateSocialMetaTags(currentPost);
// Update breadcrumb
document.getElementById('breadcrumb-title').textContent = truncate(currentPost.title, 50);
// Render post header
if (currentPost.category) {
document.getElementById('post-category').textContent = currentPost.category;
} else {
document.getElementById('post-category').style.display = 'none';
}
document.getElementById('post-title').textContent = currentPost.title;
// Author
const authorName = currentPost.author_name || 'Tractatus Team';
document.getElementById('post-author').textContent = authorName;
// Date
const publishedDate = new Date(currentPost.published_at);
const formattedDate = publishedDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
document.getElementById('post-date').textContent = formattedDate;
document.getElementById('post-date').setAttribute('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));
document.getElementById('post-read-time').textContent = `${readTime} min read`;
// Tags
if (currentPost.tags && currentPost.tags.length > 0) {
const tagsHTML = currentPost.tags.map(tag => `
${escapeHtml(tag)}
`).join('');
document.getElementById('post-tags').innerHTML = tagsHTML;
document.getElementById('post-tags-container').classList.remove('hidden');
}
// AI disclosure (if AI-assisted)
if (currentPost.ai_assisted || currentPost.metadata?.ai_assisted) {
document.getElementById('ai-disclosure').classList.remove('hidden');
}
// Post body
const bodyHTML = currentPost.content_html || convertMarkdownToHTML(currentPost.content);
document.getElementById('post-body').innerHTML = bodyHTML;
}
/**
* 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 `
${escapeHtml(post.title)}
'); html = `
${ html }
`; // Line breaks html = html.replace(/\n/g, '