updated documentation
All checks were successful
Deploy to Production / deploy (push) Successful in 6s

This commit is contained in:
Илья Глазунов 2025-12-08 01:01:45 +03:00
parent 7e1846276e
commit 58660ec8d4
22 changed files with 1091 additions and 17 deletions

View File

@ -1,3 +1,88 @@
# docs.pyserve.org
This repository contains the source files for the documentation of the PyServe project, which can be found at [docs.pyserve.org](https://docs.pyserve.org).
## Structure
```
docs/
├── index.html # Main documentation page
├── blog.html # Blog listing page
├── blog-post.html # Individual blog post template
├── style.css # Global styles with highlight.js integration
├── getting-started/ # Getting started guides
│ ├── index.html
│ ├── installation.html
│ └── quickstart.html
├── guides/ # User guides
│ ├── index.html
│ ├── asgi-mount.html
│ ├── configuration.html
│ ├── process-orchestration.html
│ ├── reverse-proxy.html
│ └── routing.html
├── reference/ # API reference
│ ├── index.html
│ ├── asgi-mount.html
│ ├── cli.html
│ └── extensions.html
└── scripts/ # JavaScript utilities
├── blog.js
├── blog-post.js
└── version-fetcher.js
```
## Standard Page Template
All documentation pages follow this structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="../index.html">Home</a> / <a href="index.html">Section</a> / Page
</div>
<div id="content">
<!-- Page content here -->
</div>
</div>
</body>
</html>
```
## Breadcrumb Navigation Format
- Root pages: `Home / Section`
- Subsection pages: `Home / Section / Page`
- Always use `/` as separator
- Link to parent section with `index.html`
- Link to home with `../index.html` (or `index.html` from root)
## Code Highlighting
All pages include highlight.js for automatic syntax highlighting:
- CSS theme: `github-dark.min.css` (matches dark theme)
- Auto-initialization: `hljs.highlightAll()`
- Custom styles in `style.css` integrate with highlight.js
## Deployment
Documentation is deployed via Gitea Actions (see `.gitea/workflows/deploy.yml`)

99
blog-post.html Normal file
View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="page-title">Post - pyserve</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="scripts/blog-post.js" defer></script>
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="index.html">Home</a> / <a href="blog.html">Blog</a> / <span id="breadcrumb-title">Post</span>
</div>
<div id="main-wrapper">
<div id="content">
<div id="post-loading" class="loading-state">
<p>Loading post...</p>
</div>
<article id="post-content" class="blog-post-full" style="display: none;">
<div class="post-meta">
<span class="post-date" id="post-date"></span>
<span class="post-tag" id="post-tag"></span>
</div>
<h1 id="post-title" class="post-title-full"></h1>
<div class="post-author-full" id="post-author-full" style="display: none;">
<img src="" alt="" class="author-avatar-large" id="author-avatar">
<div class="author-info-full">
<span class="author-name-large" id="author-name"></span>
<span class="author-login" id="author-login"></span>
</div>
</div>
<div id="post-body" class="post-body-full"></div>
<div class="post-footer">
<a href="blog.html" class="back-link">← Back to Blog</a>
</div>
</article>
<div id="post-error" class="error-state" style="display: none;">
<h2>Post Not Found</h2>
<p>The requested blog post could not be found.</p>
<a href="blog.html" class="back-link">← Back to Blog</a>
</div>
</div>
<aside id="sidebar">
<div class="partner-block">
<h3>Share</h3>
<div style="margin-top: 15px;">
<button onclick="copyLink()" class="share-button">📋 Copy Link</button>
</div>
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #2a2a2a;">
<h4 style="color: #3cb371; font-size: 13px; margin-bottom: 10px;">Navigation</h4>
<ul class="plain" style="font-size: 12px;">
<li><a href="blog.html">All Posts</a></li>
<li><a href="index.html">Documentation</a></li>
<li><a href="https://git.pyserve.org/Shifty/docs.pyserve.org" target="_blank">Repository</a></li>
</ul>
</div>
</div>
</aside>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
<script>
function copyLink() {
navigator.clipboard.writeText(window.location.href).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✓ Copied!';
btn.style.background = '#3cb371';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 2000);
});
}
</script>
</body>
</html>

65
blog.html Normal file
View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog - pyserve</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="scripts/blog.js" defer></script>
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">python application orchestrator</div>
</div>
<div class="breadcrumb">
<a href="index.html">Home</a> / Blog
</div>
<div id="main-wrapper">
<div id="content">
<h2>Blog & Updates</h2>
<p>Latest news, updates and technical articles about pyserve.</p>
<div id="blog-loading" class="loading-state">
<p>Loading posts...</p>
</div>
<div id="blog-posts" class="blog-posts"></div>
<div id="blog-error" class="error-state" style="display: none;">
<p>Failed to load blog posts. Please try again later.</p>
</div>
</div>
<aside id="sidebar">
<div class="partner-block">
<h3>About the Blog</h3>
<p style="font-size: 12px; color: #999; line-height: 1.6;">
Blog posts are powered by Git releases from
<a href="https://git.pyserve.org/Shifty/docs.pyserve.org" target="_blank">docs.pyserve.org</a> repository.
</p>
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #2a2a2a;">
<h4 style="color: #3cb371; font-size: 13px; margin-bottom: 10px;">Categories</h4>
<ul class="plain" style="font-size: 12px;">
<li><a href="#releases">Releases</a></li>
<li><a href="#tutorials">Tutorials</a></li>
<li><a href="#updates">Updates</a></li>
</ul>
</div>
</div>
</aside>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>

View File

@ -27,6 +27,18 @@ extensions:
- "X-Forwarded-For: $remote_addr"
- "X-Real-IP: $remote_addr"
"=/api/blog/posts":
proxy_pass: "https://git.pyserve.org/api/v1/repos/Shifty/docs.pyserve.org/releases"
headers:
- "X-Forwarded-For: $remote_addr"
- "X-Real-IP: $remote_addr"
"~^/api/blog/post/(?P<tag>.+)$":
proxy_pass: "https://git.pyserve.org/api/v1/repos/Shifty/docs.pyserve.org/releases/tags/{tag}"
headers:
- "X-Forwarded-For: $remote_addr"
- "X-Real-IP: $remote_addr"
"~*\\.(css)$":
root: "./docs"
cache_control: "public, max-age=3600"

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Getting Started - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » Getting Started
<a href="../index.html">Home</a> / Getting Started
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Installation - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/getting-started/">Getting Started</a> » Installation
<a href="../index.html">Home</a> / <a href="index.html">Getting Started</a> / Installation
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quick Start - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/getting-started/">Getting Started</a> » Quick Start
<a href="../index.html">Home</a> / <a href="index.html">Getting Started</a> / Quick Start
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASGI Mounting - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="index.html">Guides</a> » ASGI Mounting
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / ASGI Mounting
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Configuration
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Configuration
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guides - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » Guides
<a href="../index.html">Home</a> / Guides
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Process Orchestration - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Process Orchestration
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Process Orchestration
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reverse Proxy - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Reverse Proxy
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Reverse Proxy
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Routing - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Routing
<a href="../index.html">Home</a> / <a href="index.html">Guides</a> / Routing
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pyserve - Documentation</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<script src="scripts/version-fetcher.js" defer></script>
</head>
<body>
@ -99,6 +102,7 @@
<h2>Resources</h2>
<ul class="plain">
<li><a href="blog.html">📝 Blog &amp; Updates</a></li>
<li><a href="https://git.pyserve.org/Shifty/pyserveX">Git Repository (currently read-only for non-members)</a></li>
<li><a href="https://git.pyserve.org/Shifty/pyserveX/releases">Releases &amp; Downloads</a></li>
</ul>

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASGI Mount API - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/reference/">Reference</a> » ASGI Mount API
<a href="../index.html">Home</a> / <a href="index.html">Reference</a> / ASGI Mount API
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLI Reference - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/reference/">Reference</a> » CLI
<a href="../index.html">Home</a> / <a href="index.html">Reference</a> / CLI
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Extensions - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/reference/">Reference</a> » Extensions
<a href="../index.html">Home</a> / <a href="index.html">Reference</a> / Extensions
</div>
<div id="content">

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reference - pyserve</title>
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div id="container">
@ -14,7 +17,7 @@
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » Reference
<a href="../index.html">Home</a> / Reference
</div>
<div id="content">

179
scripts/blog-post.js Normal file
View File

@ -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 '<p>No content available.</p>';
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, '<pre><code class="language-$1">$2</code></pre>')
.replace(/^#### (.*$)/gim, '<h4>$1</h4>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^---$/gm, '<hr>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width: 100%; height: auto;">')
.replace(/^\* (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
.split('\n\n')
.map(para => {
para = para.trim();
if (para.match(/^<(h[1-6]|ul|ol|pre|blockquote|hr)/)) {
return para;
}
return para ? `<p>${para}</p>` : '';
})
.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();
}
})();

218
scripts/blog.js Normal file
View File

@ -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, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>');
return '<p>' + html + '</p>';
}
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 `
<article class="blog-post-card">
<div class="post-meta">
<span class="post-date">${date}</span>
<span class="post-tag">${tag}</span>
</div>
<h3 class="post-title">
<a href="blog-post.html?id=${tag}">${post.name || tag}</a>
</h3>
<div class="post-excerpt">${excerpt}</div>
<div class="post-author">
<img src="${authorAvatar}" alt="${authorName}" class="author-avatar" onerror="this.style.display='none'">
<div class="author-info">
<span class="author-name">${authorName}</span>
${authorLogin ? `<span class="author-login">@${authorLogin}</span>` : ''}
</div>
</div>
<a href="blog-post.html?id=${tag}" class="read-more">Read more </a>
</article>
`;
}
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 = `
<div class="note">
<strong>No posts yet</strong>
<p>Check back soon for updates and articles!</p>
</div>
`;
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();
}
})();

View File

@ -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';

376
style.css
View File

@ -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;
}