// Blog System - Powered by Gitea Releases (function() { 'use strict'; const API_URL = '/api/blog/posts'; const CACHE_KEY = 'pyserve_blog_cache'; const CACHE_DURATION = 1800000; async function fetchBlogPosts() { try { const response = await fetch(API_URL, { method: 'GET', headers: { 'Accept': 'application/json' } }); if (!response.ok) { console.warn(`Blog API request failed: ${response.status}`); return null; } const posts = await response.json(); console.log(`✓ Fetched ${posts.length} blog posts`); return posts; } catch (error) { console.error('Blog API fetch error:', error.message); return null; } } function getCachedPosts() { try { const cached = localStorage.getItem(CACHE_KEY); if (!cached) return null; const data = JSON.parse(cached); const now = Date.now(); if (now - data.timestamp < CACHE_DURATION) { return data.posts; } localStorage.removeItem(CACHE_KEY); return null; } catch (error) { console.error('Cache read error:', error); return null; } } function cachePosts(posts) { try { const data = { posts: posts, timestamp: Date.now() }; localStorage.setItem(CACHE_KEY, JSON.stringify(data)); } catch (error) { console.error('Cache write error:', error); } } function formatDate(dateString) { const date = new Date(dateString); const options = { year: 'numeric', month: 'long', day: 'numeric' }; return date.toLocaleDateString('en-US', options); } function parseMarkdown(markdown) { if (!markdown) return ''; if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') { marked.setOptions({ highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value; } return hljs.highlightAuto(code).value; }, breaks: true, gfm: true }); return marked.parse(markdown); } let html = markdown .replace(/^### (.*$)/gim, '

$1

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

$1

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

$1

') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/```(\w+)?\n([\s\S]*?)```/g, '
$2
') .replace(/`([^`]+)`/g, '$1') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') .replace(/\n\n/g, '

') .replace(/\n/g, '
'); return '

' + html + '

'; } function extractExcerpt(markdown, maxLength = 200) { if (!markdown) return 'No description available.'; // Remove markdown formatting and get first paragraph const plain = markdown .replace(/^#+\s+.*/gm, '') // Remove headers .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links .replace(/```[\s\S]*?```/g, '') // Remove code blocks .replace(/`([^`]+)`/g, '$1') // Remove inline code .trim(); const firstParagraph = plain.split('\n\n')[0] || plain.split('\n')[0]; if (firstParagraph.length > maxLength) { return firstParagraph.substring(0, maxLength) + '...'; } return firstParagraph || 'No description available.'; } function createPostCard(post) { const excerpt = extractExcerpt(post.body); const date = formatDate(post.published_at || post.created_at); const tag = post.tag_name || 'post'; const author = post.author || {}; const authorName = author.full_name || author.login || 'Unknown'; const authorLogin = author.login || ''; const authorAvatar = author.avatar_url || ''; return `

${post.name || tag}

${excerpt}
${authorName}
${authorName} ${authorLogin ? `` : ''}
Read more →
`; } function renderPosts(posts) { const container = document.getElementById('blog-posts'); const loading = document.getElementById('blog-loading'); const error = document.getElementById('blog-error'); if (!container) return; loading.style.display = 'none'; if (!posts || posts.length === 0) { container.innerHTML = `
No posts yet

Check back soon for updates and articles!

`; return; } // Sort by date (newest first) const sortedPosts = posts.sort((a, b) => { const dateA = new Date(a.published_at || a.created_at); const dateB = new Date(b.published_at || b.created_at); return dateB - dateA; }); // Render posts container.innerHTML = sortedPosts.map(post => createPostCard(post)).join(''); } function showError() { const loading = document.getElementById('blog-loading'); const error = document.getElementById('blog-error'); if (loading) loading.style.display = 'none'; if (error) error.style.display = 'block'; } async function init() { // Try cached posts first const cachedPosts = getCachedPosts(); if (cachedPosts) { renderPosts(cachedPosts); } // Fetch fresh posts in background const posts = await fetchBlogPosts(); if (posts) { cachePosts(posts); renderPosts(posts); } else if (!cachedPosts) { showError(); } } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();