diff --git a/README.md b/README.md index 515a80a..68db5f7 100644 --- a/README.md +++ b/README.md @@ -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 + + + + + + Page Title - pyserve + + + + + + +
+ + + + +
+ +
+
+ + +``` + +## 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`) diff --git a/blog-post.html b/blog-post.html new file mode 100644 index 0000000..8bd83d1 --- /dev/null +++ b/blog-post.html @@ -0,0 +1,99 @@ + + + + + + Post - pyserve + + + + + + + +
+ + + + +
+
+
+

Loading post...

+
+ + + + +
+ + +
+ + +
+ + + + diff --git a/blog.html b/blog.html new file mode 100644 index 0000000..23abcb3 --- /dev/null +++ b/blog.html @@ -0,0 +1,65 @@ + + + + + + Blog - pyserve + + + + + + + +
+ + + + +
+
+

Blog & Updates

+

Latest news, updates and technical articles about pyserve.

+ +
+

Loading posts...

+
+ +
+ + +
+ + +
+ + +
+ + diff --git a/config.docs.yaml b/config.docs.yaml index f4b59b9..6cd103b 100644 --- a/config.docs.yaml +++ b/config.docs.yaml @@ -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.+)$": + 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" diff --git a/getting-started/index.html b/getting-started/index.html index b011dc8..6fc07ed 100644 --- a/getting-started/index.html +++ b/getting-started/index.html @@ -5,6 +5,9 @@ Getting Started - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/getting-started/installation.html b/getting-started/installation.html index 91d3c83..4dc1f09 100644 --- a/getting-started/installation.html +++ b/getting-started/installation.html @@ -5,6 +5,9 @@ Installation - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/getting-started/quickstart.html b/getting-started/quickstart.html index f3c0326..422a00f 100644 --- a/getting-started/quickstart.html +++ b/getting-started/quickstart.html @@ -5,6 +5,9 @@ Quick Start - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/guides/asgi-mount.html b/guides/asgi-mount.html index 2d0894d..da3df53 100644 --- a/guides/asgi-mount.html +++ b/guides/asgi-mount.html @@ -5,6 +5,9 @@ ASGI Mounting - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/guides/configuration.html b/guides/configuration.html index 0a6a841..c57bcc6 100644 --- a/guides/configuration.html +++ b/guides/configuration.html @@ -5,6 +5,9 @@ Configuration - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/guides/index.html b/guides/index.html index 725b04e..86859cf 100644 --- a/guides/index.html +++ b/guides/index.html @@ -5,6 +5,9 @@ Guides - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/guides/process-orchestration.html b/guides/process-orchestration.html index b6f9a14..fb775e3 100644 --- a/guides/process-orchestration.html +++ b/guides/process-orchestration.html @@ -5,6 +5,9 @@ Process Orchestration - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/guides/reverse-proxy.html b/guides/reverse-proxy.html index 255d14f..acd152f 100644 --- a/guides/reverse-proxy.html +++ b/guides/reverse-proxy.html @@ -5,6 +5,9 @@ Reverse Proxy - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/guides/routing.html b/guides/routing.html index 982c808..233059e 100644 --- a/guides/routing.html +++ b/guides/routing.html @@ -5,6 +5,9 @@ Routing - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/index.html b/index.html index a9470e8..541b092 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,9 @@ pyserve - Documentation + + + @@ -99,6 +102,7 @@

Resources

diff --git a/reference/asgi-mount.html b/reference/asgi-mount.html index 1a8a97e..46c61b3 100644 --- a/reference/asgi-mount.html +++ b/reference/asgi-mount.html @@ -5,6 +5,9 @@ ASGI Mount API - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/reference/cli.html b/reference/cli.html index 89111ef..5015a58 100644 --- a/reference/cli.html +++ b/reference/cli.html @@ -5,6 +5,9 @@ CLI Reference - pyserve + + +
@@ -14,7 +17,7 @@
diff --git a/reference/extensions.html b/reference/extensions.html index 1c1cd04..f0e207e 100644 --- a/reference/extensions.html +++ b/reference/extensions.html @@ -5,6 +5,9 @@ Extensions - pyserve + + +
@@ -14,7 +17,7 @@
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 @@ Reference - pyserve + + +
@@ -14,7 +17,7 @@
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, '$1') + .replace(/^\* (.+)$/gm, '
  • $1
  • ') + .replace(/(
  • .*<\/li>)/s, '
      $1
    ') + .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 ` + + `; + } + + 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; +}