diff --git a/reference/index.html b/reference/index.html
index 74209ee..173c50d 100644
--- a/reference/index.html
+++ b/reference/index.html
@@ -5,6 +5,9 @@
diff --git a/scripts/blog-post.js b/scripts/blog-post.js
new file mode 100644
index 0000000..38b3b72
--- /dev/null
+++ b/scripts/blog-post.js
@@ -0,0 +1,179 @@
+// Blog Post Viewer
+
+(function() {
+ 'use strict';
+
+ function getPostId() {
+ const params = new URLSearchParams(window.location.search);
+ return params.get('id');
+ }
+
+ async function fetchPost(postId) {
+ try {
+ const response = await fetch(`/api/blog/post/${postId}`, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ console.warn(`Post API request failed: ${response.status}`);
+ return null;
+ }
+
+ const post = await response.json();
+ console.log(`✓ Fetched post: ${post.name}`);
+ return post;
+
+ } catch (error) {
+ console.error('Post API fetch error:', error.message);
+ return null;
+ }
+ }
+
+ /**
+ * Format date to readable string
+ */
+ 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 '
No content available.
';
+
+ 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(/```(\w+)?\n([\s\S]*?)```/g, '
$2
')
+ .replace(/^#### (.*$)/gim, '
$1
')
+ .replace(/^### (.*$)/gim, '
$1
')
+ .replace(/^## (.*$)/gim, '
$1
')
+ .replace(/^# (.*$)/gim, '
$1
')
+ .replace(/^---$/gm, '
')
+ .replace(/\*\*(.+?)\*\*/g, '
$1')
+ .replace(/\*(.+?)\*/g, '
$1')
+ .replace(/`([^`]+)`/g, '
$1')
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '
$1')
+ .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '

')
+ .replace(/^\* (.+)$/gm, '
$1')
+ .replace(/(
.*<\/li>)/s, '')
+ .replace(/^\d+\. (.+)$/gm, '$1')
+ .replace(/^> (.+)$/gm, '
$1
')
+ .split('\n\n')
+ .map(para => {
+ para = para.trim();
+ if (para.match(/^<(h[1-6]|ul|ol|pre|blockquote|hr)/)) {
+ return para;
+ }
+ return para ? `
${para}
` : '';
+ })
+ .join('\n');
+
+ return html;
+ }
+
+ /**
+ * Render post to DOM
+ */
+ function renderPost(post) {
+ const loading = document.getElementById('post-loading');
+ const content = document.getElementById('post-content');
+ const error = document.getElementById('post-error');
+
+ if (!post) {
+ loading.style.display = 'none';
+ error.style.display = 'block';
+ return;
+ }
+
+ loading.style.display = 'none';
+ content.style.display = 'block';
+
+ // Update page title
+ document.getElementById('page-title').textContent = `${post.name} - pyserve`;
+ document.getElementById('breadcrumb-title').textContent = post.name;
+
+ // Update meta
+ const date = formatDate(post.published_at || post.created_at);
+ document.getElementById('post-date').textContent = date;
+ document.getElementById('post-tag').textContent = post.tag_name || 'post';
+
+ // Update title
+ document.getElementById('post-title').textContent = post.name;
+
+ // Parse and render body
+ const bodyHtml = parseMarkdown(post.body);
+ document.getElementById('post-body').innerHTML = bodyHtml;
+
+ // Update author info
+ if (post.author) {
+ const authorSection = document.getElementById('post-author-full');
+ const avatarImg = document.getElementById('author-avatar');
+ const authorName = document.getElementById('author-name');
+ const authorLogin = document.getElementById('author-login');
+
+ if (avatarImg) {
+ avatarImg.src = post.author.avatar_url || 'https://via.placeholder.com/80';
+ avatarImg.alt = post.author.full_name || post.author.login;
+ }
+ if (authorName) authorName.textContent = post.author.full_name || post.author.login;
+ if (authorLogin) authorLogin.textContent = `@${post.author.login}`;
+
+ authorSection.style.display = 'flex';
+ }
+ }
+
+ /**
+ * Show error state
+ */
+ function showError() {
+ const loading = document.getElementById('post-loading');
+ const error = document.getElementById('post-error');
+
+ loading.style.display = 'none';
+ error.style.display = 'block';
+ }
+
+ /**
+ * Main initialization
+ */
+ async function init() {
+ const postId = getPostId();
+
+ if (!postId) {
+ showError();
+ return;
+ }
+
+ const post = await fetchPost(postId);
+
+ if (post) {
+ renderPost(post);
+ } else {
+ showError();
+ }
+ }
+
+ // Start when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+
+})();
diff --git a/scripts/blog.js b/scripts/blog.js
new file mode 100644
index 0000000..fb2e5c2
--- /dev/null
+++ b/scripts/blog.js
@@ -0,0 +1,218 @@
+// 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 `
+
+
+ ${date}
+ ${tag}
+
+
+ ${excerpt}
+
+

+
+ ${authorName}
+ ${authorLogin ? `@${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();
+ }
+
+})();
diff --git a/scripts/version-fetcher.js b/scripts/version-fetcher.js
index 748adbd..972c5dc 100644
--- a/scripts/version-fetcher.js
+++ b/scripts/version-fetcher.js
@@ -1,7 +1,4 @@
-/**
- * Version Fetcher - Automatically fetches and displays latest pyserveX version
- * from Gitea releases API.
- */
+// Version Fetcher - Displays latest pyserveX version from Gitea API
(function() {
'use strict';
diff --git a/style.css b/style.css
index 402e70a..34427e2 100644
--- a/style.css
+++ b/style.css
@@ -440,3 +440,379 @@ dd {
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
+
+/* Blog styles */
+.blog-posts {
+ margin-top: 20px;
+}
+
+.blog-post-card {
+ background: #0d0d0d;
+ border: 1px solid #333;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+ transition: all 0.3s ease;
+}
+
+.blog-post-card:hover {
+ border-color: #3cb371;
+ box-shadow: 0 4px 12px rgba(60, 179, 113, 0.1);
+}
+
+.post-meta {
+ display: flex;
+ gap: 15px;
+ align-items: center;
+ margin-bottom: 10px;
+ font-size: 12px;
+}
+
+.post-date {
+ color: #888;
+}
+
+.post-tag {
+ background: #1a2a1a;
+ color: #3cb371;
+ padding: 2px 8px;
+ border-radius: 3px;
+ font-weight: bold;
+}
+
+.post-author {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid #2a2a2a;
+}
+
+.author-avatar {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: #2a2a2a;
+}
+
+.author-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.author-name {
+ color: #e0e0e0;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.author-login {
+ color: #888;
+ font-size: 11px;
+}
+
+.post-title {
+ margin: 10px 0;
+ font-size: 18px;
+}
+
+.post-title a {
+ color: #e0e0e0;
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.post-title a:hover {
+ color: #3cb371;
+}
+
+.post-excerpt {
+ color: #999;
+ font-size: 13px;
+ line-height: 1.6;
+ margin: 10px 0 15px 0;
+}
+
+.read-more {
+ color: #5fba7d;
+ font-size: 13px;
+ font-weight: bold;
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.read-more:hover {
+ color: #7ccd9a;
+}
+
+.loading-state,
+.error-state {
+ text-align: center;
+ padding: 40px 20px;
+ color: #888;
+}
+
+.error-state {
+ color: #b8860b;
+}
+
+/* Blog post full view */
+.blog-post-full {
+ max-width: 800px;
+}
+
+.post-title-full {
+ font-size: 28px;
+ color: #e0e0e0;
+ margin: 15px 0 20px 0;
+ line-height: 1.3;
+}
+
+.post-body-full {
+ color: #c9c9c9;
+ font-size: 14px;
+ line-height: 1.7;
+}
+
+.post-body-full h1 {
+ font-size: 24px;
+ margin: 30px 0 15px 0;
+ color: #e0e0e0;
+ border-bottom: 2px solid #2e8b57;
+ padding-bottom: 8px;
+}
+
+.post-body-full h2 {
+ font-size: 20px;
+ margin: 25px 0 12px 0;
+ color: #e0e0e0;
+ border-bottom: 1px solid #333;
+ padding-bottom: 5px;
+}
+
+.post-body-full h3 {
+ font-size: 17px;
+ margin: 20px 0 10px 0;
+ color: #d0d0d0;
+}
+
+.post-body-full h4 {
+ font-size: 15px;
+ margin: 15px 0 8px 0;
+ color: #d0d0d0;
+}
+
+.post-body-full p {
+ margin: 12px 0;
+}
+
+.post-body-full ul,
+.post-body-full ol {
+ margin: 12px 0;
+ padding-left: 30px;
+}
+
+.post-body-full li {
+ margin: 5px 0;
+}
+
+.post-body-full pre {
+ background: #0d0d0d;
+ border: 1px solid #333;
+ border-radius: 4px;
+ padding: 15px;
+ overflow-x: auto;
+ margin: 15px 0;
+}
+
+.post-body-full code {
+ background: #0d0d0d;
+ border: 1px solid #333;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+ font-size: 13px;
+}
+
+.post-body-full pre code {
+ border: none;
+ padding: 0;
+ background: transparent;
+}
+
+.post-body-full blockquote {
+ border-left: 3px solid #3cb371;
+ margin: 15px 0;
+ padding: 10px 20px;
+ background: #1a2a1a;
+ font-style: italic;
+ color: #999;
+}
+
+.post-body-full hr {
+ border: none;
+ border-top: 1px solid #333;
+ margin: 25px 0;
+}
+
+.post-body-full img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 4px;
+ margin: 15px 0;
+}
+
+.post-footer {
+ margin-top: 40px;
+ padding-top: 20px;
+ border-top: 1px solid #333;
+}
+
+.back-link {
+ color: #5fba7d;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.back-link:hover {
+ color: #7ccd9a;
+ text-decoration: underline;
+}
+
+.share-button {
+ background: #0d0d0d;
+ border: 1px solid #3cb371;
+ color: #3cb371;
+ padding: 8px 16px;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: bold;
+ transition: all 0.2s ease;
+ width: 100%;
+}
+
+.share-button:hover {
+ background: #3cb371;
+ color: #fff;
+}
+
+.post-author-full {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 15px;
+ background: #0d0d0d;
+ border: 1px solid #333;
+ border-radius: 6px;
+ margin: 20px 0;
+}
+
+.author-avatar-large {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: #2a2a2a;
+}
+
+.author-info-full {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.author-name-large {
+ color: #e0e0e0;
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.post-body-full pre {
+ background: #0d0d0d;
+ border: 1px solid #333;
+ border-radius: 6px;
+ padding: 16px;
+ overflow-x: auto;
+ margin: 15px 0;
+}
+
+.post-body-full code {
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+ font-size: 13px;
+}
+
+.post-body-full pre code {
+ background: transparent;
+ border: none;
+ padding: 0;
+}
+
+/* Highlight.js integration */
+pre code.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1em;
+}
+
+code.hljs {
+ padding: 3px 5px;
+}
+
+/* Ensure highlight.js styles work with our dark theme */
+.hljs {
+ background: #0d0d0d !important;
+ color: #b0b0b0;
+}
+
+.hljs-comment,
+.hljs-quote {
+ color: #6a9955;
+ font-style: italic;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-literal,
+.hljs-type {
+ color: #569cd6;
+}
+
+.hljs-string,
+.hljs-title,
+.hljs-section {
+ color: #ce9178;
+}
+
+.hljs-name,
+.hljs-attribute {
+ color: #9cdcfe;
+}
+
+.hljs-variable,
+.hljs-template-variable {
+ color: #4ec9b0;
+}
+
+.hljs-number {
+ color: #b5cea8;
+}
+
+.hljs-built_in,
+.hljs-builtin-name {
+ color: #4ec9b0;
+}
+
+.hljs-meta {
+ color: #808080;
+}
+
+.hljs-emphasis {
+ font-style: italic;
+}
+
+.hljs-strong {
+ font-weight: bold;
+}