Compare commits

...

10 Commits

Author SHA1 Message Date
c3c74bb21f i think this might work 2025-07-27 17:13:21 +02:00
e3d8ba1017 Update environment configuration, enhance deployment script, and localize backend messages
All checks were successful
Deploy / build-and-deploy (push) Successful in 31m44s
- Added instructions in .env.local for Docker deployment.
- Improved docker.sh to display deployment status with colored output and added ASCII art.
- Updated main.js to indicate future deprecation of the Electron app.
- Translated various log messages and CLI command outputs in the Rust backend to German for better localization.
- Removed unused asset (peta.png) from the project.
- Updated RustStatusPage component to reflect German translations in UI elements and error messages.
2025-07-06 21:17:22 +02:00
a9879d9fa4 flowcharts
All checks were successful
Deploy / build-and-deploy (push) Successful in 31m22s
2025-07-06 18:02:41 +02:00
7d387080f5 Remove Docker image push step from deployment workflow
All checks were successful
Deploy / build-and-deploy (push) Successful in 31m9s
2025-07-06 12:11:18 +02:00
09a673b65b fuck me
Some checks failed
Deploy / build-and-deploy (push) Failing after 21m46s
2025-07-06 11:49:10 +02:00
6665f65529 did some docker stuff ; deploy this shit .
Some checks failed
Deploy / build-and-deploy (push) Failing after 22m33s
2025-07-06 11:17:22 +02:00
7629164387 Merge pull request 'vim' (#12) from vim into main
Some checks failed
Deploy / build-and-deploy (push) Failing after 1s
Reviewed-on: http://10.0.0.13:3002/rattatwinko/markdownblog/pulls/12

holy hell if this goes wrong im cooked
2025-07-05 20:25:39 +00:00
21f13ef8ae Enhance blog features and improve backend functionality
- Added a VS Code-style editor with YAML frontmatter support and live preview.
- Implemented force reparse functionality for immediate updates of posts.
- Improved directory scanning with error handling and automatic directory creation.
- Introduced new CLI commands for cache management: `reinterpret-all` and `reparse-post`.
- Enhanced logging for better debugging and monitoring of the Rust backend.
- Updated README to reflect new features and improvements.
2025-07-05 22:23:58 +02:00
f94ddaa3b1 ner 2025-07-05 21:23:05 +02:00
559abe3933 fucking shit working properly now. prod works.
TODO; Fix asset loading :3
2025-07-05 16:01:29 +02:00
32 changed files with 1732 additions and 400 deletions

View File

@@ -1,6 +1,9 @@
#-------------------------------------------------------------------- # -----------------------------------------------------------------------# #-------------------------------------------------------------------- # -----------------------------------------------------------------------#
# In here you have to set your socials / links # Explenations of Variables # # In here you have to set your socials / links # Explenations of Variables #
#-------------------------------------------------------------------- # -----------------------------------------------------------------------# #-------------------------------------------------------------------- # -----------------------------------------------------------------------#
# Modify This before deploying with docker / locally #
#---------------------------------------------------------------------#
#
NEXT_PUBLIC_BLOG_OWNER=Rattatwinko # Your Name goes here # NEXT_PUBLIC_BLOG_OWNER=Rattatwinko # Your Name goes here #
NEXT_ABOUT_ME_LINK="http://localhost:80" # Your WebPage goes here # NEXT_ABOUT_ME_LINK="http://localhost:80" # Your WebPage goes here #
NEXT_SOCIAL_INSTAGRAM="http://instagram.com/rattatwinko" # Your Instagram Link goes here # NEXT_SOCIAL_INSTAGRAM="http://instagram.com/rattatwinko" # Your Instagram Link goes here #
@@ -9,3 +12,4 @@ NEXT_SOCIAL_GITHUB_STATE="true" # I Have G
NEXT_SOCIAL_GITHUB_LINK_IF_TRUE="http://github.com/ZockerKatze" # If you have GitHub then paste your link here # NEXT_SOCIAL_GITHUB_LINK_IF_TRUE="http://github.com/ZockerKatze" # If you have GitHub then paste your link here #
NEXT_SOCIAL_BUYMEACOFFEE="https://coff.ee/rattatwinko" NEXT_SOCIAL_BUYMEACOFFEE="https://coff.ee/rattatwinko"
PORT=8080 # This is unused. You can safely delete if you want. # PORT=8080 # This is unused. You can safely delete if you want. #
BASE_URL=/blog # This is the subpath!

View File

@@ -10,19 +10,52 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
run: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install Node.js dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
- name: Cache Rust dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
markdown_backend/target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Check Rust code
working-directory: markdown_backend
run: cargo check
- name: Install Docker - name: Install Docker
run: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Build Docker image - name: Build Docker image
run: docker build -t markdownblog . run: docker build -t localhost:3002/rattatwinko/markdownblog:latest .
- name: Save Docker image as tarball - name: Save Docker image as tarball
run: docker save markdownblog -o markdownblog-image.tar run: docker save localhost:3002/rattatwinko/markdownblog:latest -o markdownblog-image.tar
- name: Upload Docker image artifact - name: Upload Docker image artifact
run: actions/upload-artifact@v3 --name markdownblog-docker-image --path markdownblog-image.tar uses: actions/upload-artifact@v3
with:
- name: Push Docker image name: markdownblog-docker-image
run: docker push 10.0.0.13:3002/rattatwinko/markdownblog:latest path: markdownblog-image.tar

127
README.md
View File

@@ -20,6 +20,9 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, **
- **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion - **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion
- **📦 Export Functionality**: Export all posts as tar.gz archive (Docker only) - **📦 Export Functionality**: Export all posts as tar.gz archive (Docker only)
- **💾 Smart Caching**: RAM-based caching system for instant post retrieval - **💾 Smart Caching**: RAM-based caching system for instant post retrieval
- **🔧 VS Code-Style Editor**: Monaco editor with YAML frontmatter support and live preview
- **🔄 Force Reparse**: Manual cache clearing and post reparsing for immediate updates
- **📁 Reliable Directory Scanning**: Robust file system traversal with error handling
--- ---
@@ -43,13 +46,15 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, **
markdownblog/ markdownblog/
├── markdown_backend/ # Rust backend for markdown processing ├── markdown_backend/ # Rust backend for markdown processing
│ ├── src/ │ ├── src/
│ │ ├── main.rs # CLI interface and command handling │ │ ├── main.rs # CLI interface and command handling
│ │ └── markdown.rs # Markdown parsing, caching, and file watching │ │ └── markdown.rs # Markdown parsing, caching, and file watching
│ ├── Cargo.toml # Rust dependencies and configuration │ ├── Cargo.toml # Rust dependencies and configuration
│ └── target/ # Compiled Rust binaries │ └── target/ # Compiled Rust binaries
├── src/ ├── src/
│ ├── app/ # Next.js 14 App Router │ ├── app/ # Next.js 14 App Router
│ │ ├── admin/ # Admin dashboard pages │ │ ├── admin/ # Admin dashboard pages
│ │ │ ├── editor/ # VS Code-style editor
│ │ │ │ └── page.tsx # Markdown editor with Monaco
│ │ │ ├── manage/ # Content management interface │ │ │ ├── manage/ # Content management interface
│ │ │ │ ├── page.tsx # Manage posts and folders │ │ │ │ ├── page.tsx # Manage posts and folders
│ │ │ │ └── rust-status/ # Rust backend monitoring │ │ │ │ └── rust-status/ # Rust backend monitoring
@@ -84,8 +89,12 @@ markdownblog/
│ │ │ └── posts/ # Public post API │ │ │ └── posts/ # Public post API
│ │ │ ├── [slug]/ # Dynamic post API routes │ │ │ ├── [slug]/ # Dynamic post API routes
│ │ │ │ └── route.ts │ │ │ │ └── route.ts
│ │ │ ├── preview/ # Markdown preview API
│ │ │ │ └── route.ts
│ │ │ ├── stream/ # Server-Sent Events for real-time updates │ │ │ ├── stream/ # Server-Sent Events for real-time updates
│ │ │ │ └── route.ts │ │ │ │ └── route.ts
│ │ │ ├── webhook/ # Webhook endpoint
│ │ │ │ └── route.ts
│ │ │ └── route.ts # List all posts │ │ │ └── route.ts # List all posts
│ │ ├── posts/ # Blog post pages │ │ ├── posts/ # Blog post pages
│ │ │ └── [...slug]/ # Dynamic post routing (catch-all) │ │ │ └── [...slug]/ # Dynamic post routing (catch-all)
@@ -97,15 +106,15 @@ markdownblog/
│ │ ├── highlight-github.css # Code syntax highlighting styles │ │ ├── highlight-github.css # Code syntax highlighting styles
│ │ ├── layout.tsx # Root layout with metadata │ │ ├── layout.tsx # Root layout with metadata
│ │ ├── MobileNav.tsx # Mobile navigation component │ │ ├── MobileNav.tsx # Mobile navigation component
│ │ ├── monaco-vim.d.ts # Monaco Vim typings
│ │ └── page.tsx # Homepage with post listing │ │ └── page.tsx # Homepage with post listing
│ └── lib/ # Utility libraries │ └── lib/ # Utility libraries
│ └── postsDirectory.ts # Post directory management and Rust integration │ └── postsDirectory.ts # Post directory management and Rust integration
├── posts/ # Markdown blog posts storage ├── posts/ # Markdown blog posts storage
│ ├── pinned.json # Pinned posts configuration │ ├── about.md
│ ├── welcome.md # Welcome post with frontmatter │ ├── welcome.md
── mdtest.md # Test post with various markdown features ── assets/
├── anchor-test.md # Test post for anchor linking └── peta.png
│ └── ii/ # Example nested folder structure
├── public/ # Static assets ├── public/ # Static assets
│ ├── android-chrome-192x192.png │ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png │ ├── android-chrome-512x512.png
@@ -115,19 +124,19 @@ markdownblog/
│ ├── favicon.ico │ ├── favicon.ico
│ └── site.webmanifest │ └── site.webmanifest
├── electron/ # Desktop application ├── electron/ # Desktop application
│ └── main.js # Electron main process configuration │ └── main.js # Electron main process configuration
├── Dockerfile # Docker container configuration ├── Dockerfile # Docker container configuration
├── docker.sh # Docker deployment script ├── docker.sh # Docker deployment script
├── entrypoint.sh # Container entrypoint script ├── entrypoint.sh # Container entrypoint script
├── run-local-backend.sh # Local Rust backend runner ├── run-local-backend.sh # Local Rust backend runner
├── next-env.d.ts # Next.js TypeScript definitions ├── next-env.d.ts # Next.js TypeScript definitions
├── next.config.js # Next.js configuration ├── next.config.js # Next.js configuration
├── package-lock.json # npm lock file ├── package-lock.json # npm lock file
├── package.json # Dependencies and scripts ├── package.json # Dependencies and scripts
├── postcss.config.js # PostCSS configuration ├── postcss.config.js # PostCSS configuration
├── tailwind.config.js # Tailwind CSS configuration ├── tailwind.config.js # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration ├── tsconfig.json # TypeScript configuration
└── LICENSE # MIT License └── LICENSE # MIT License
``` ```
### Key Components ### Key Components
@@ -329,6 +338,9 @@ console.log("Hello, World!");
- **📦 Export Posts**: Download all posts as archive (Docker only) - **📦 Export Posts**: Download all posts as archive (Docker only)
- **📊 Rust Status**: Monitor parser performance, logs, and health - **📊 Rust Status**: Monitor parser performance, logs, and health
- **🔍 Log Management**: View, filter, and clear parser logs - **🔍 Log Management**: View, filter, and clear parser logs
- **🔧 VS Code Editor**: Monaco-based editor with YAML frontmatter preservation
- **🔄 Force Reparse**: Manual cache clearing and post reparsing
- **📁 Reliable Scanning**: Enhanced directory traversal with error recovery
### Security ### Security
@@ -350,6 +362,9 @@ console.log("Hello, World!");
- **📁 Recursive Scanning**: Efficient folder traversal and file discovery - **📁 Recursive Scanning**: Efficient folder traversal and file discovery
- **💾 Smart Caching**: RAM-based caching with disk persistence - **💾 Smart Caching**: RAM-based caching with disk persistence
- **📊 Performance Monitoring**: Real-time metrics and logging - **📊 Performance Monitoring**: Real-time metrics and logging
- **🔄 Force Reparse**: Manual cache invalidation and post reparsing
- **📁 Reliable Directory Scanning**: Robust error handling and recovery
- **🔧 Single Post Reparse**: Efficient individual post cache clearing
### Real-Time Updates ### Real-Time Updates
@@ -358,6 +373,16 @@ console.log("Hello, World!");
- **⚡ Instant Updates**: Sub-second response to file modifications - **⚡ Instant Updates**: Sub-second response to file modifications
- **🔄 Fallback Polling**: Graceful degradation if SSE fails - **🔄 Fallback Polling**: Graceful degradation if SSE fails
### VS Code-Style Editor
- **🔧 Monaco Editor**: Professional code editor with syntax highlighting
- **📄 YAML Frontmatter**: Preserved and editable at the top of files
- **👁️ Live Preview**: Real-time Markdown rendering
- **💾 Save & Reparse**: Automatic cache clearing and post reparsing
- **⌨️ Vim Mode**: Optional Vim keybindings for power users
- **📱 Responsive Design**: Works on desktop and mobile devices
- **🎨 Custom Styling**: JetBrains Mono font and VS Code-like appearance
--- ---
## 🎨 Customization ## 🎨 Customization
@@ -398,6 +423,8 @@ cargo build --release # Build optimized binary
cargo run -- watch # Watch for file changes cargo run -- watch # Watch for file changes
cargo run -- logs # View parser logs cargo run -- logs # View parser logs
cargo run -- checkhealth # Check backend health cargo run -- checkhealth # Check backend health
cargo run -- reinterpret-all # Force reparse all posts
cargo run -- reparse-post <slug> # Force reparse single post
``` ```
--- ---
@@ -434,7 +461,65 @@ MIT License - see [LICENSE](LICENSE) file for details.
- **File watching issues**: Check file permissions and inotify limits - **File watching issues**: Check file permissions and inotify limits
- **Performance issues**: Monitor logs via admin interface - **Performance issues**: Monitor logs via admin interface
- **Cache problems**: Clear cache via admin interface or restart - **Cache problems**: Clear cache via admin interface or restart
- **Directory scanning errors**: Check file permissions and hidden files
- **Reparse failures**: Verify post slugs and file existence
- **Memory issues**: Monitor cache size and clear if necessary
### Support ### Support
For issues and questions, please check the project structure and API documentation in the codebase. The admin interface includes comprehensive monitoring tools for the Rust backend. For issues and questions, please check the project structure and API documentation in the codebase. The admin interface includes comprehensive monitoring tools for the Rust backend.
---
## 🆕 Recent Improvements (Latest)
### Rust Backend Enhancements
- **🔄 Force Reparse Commands**: New CLI commands for manual cache invalidation
- `reinterpret-all`: Clear all caches and reparse every post
- `reparse-post <slug>`: Clear cache for specific post and reparse
- **📁 Reliable Directory Scanning**: Enhanced file system traversal with:
- Hidden file filtering (skips `.` files)
- Graceful error recovery for inaccessible files
- Detailed logging of scanning process
- Automatic directory creation if missing
- **💾 Improved Cache Management**: Better cache directory handling and persistence
- **📊 Enhanced Logging**: Comprehensive logging for debugging and monitoring
### Editor Improvements
- **🔧 VS Code-Style Interface**: Monaco editor with professional features
- **📄 YAML Frontmatter Preservation**: Frontmatter stays at top and remains editable
- **💾 Save & Reparse Integration**: Automatic Rust backend integration on save
- **👁️ Live Preview**: Real-time Markdown rendering without frontmatter
- **⌨️ Vim Mode Support**: Optional Vim keybindings for power users
- **📱 Mobile Responsive**: Works seamlessly on all device sizes
### Admin Panel Enhancements
- **🔄 Force Reparse Button**: One-click cache clearing and post reparsing
- **📊 Enhanced Rust Status**: Real-time parser performance monitoring
- **🔍 Improved Log Management**: Better filtering and search capabilities
- **📁 Directory Health Monitoring**: Comprehensive file system diagnostics
## Configuring a Base URL for Proxy Hosting
If you want to host your app behind a subpath (e.g. `http://localhost:3000/blog/`), set the base URL in `.env.local`:
```
BASE_URL=/blog
```
This will automatically prefix all internal links, API calls, and static assets with `/blog`. Make sure your reverse proxy (e.g. nginx) is configured to forward requests from `/blog` to your app.
### Example nginx config
```
location /blog/ {
proxy_pass http://localhost:3000/blog/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```

View File

@@ -44,5 +44,37 @@ if ! docker ps | grep -q $CONTAINER_NAME; then
exit 1 exit 1
fi fi
# Output with colors
GREEN='\033[1;32m' # Green
CYAN='\033[1;36m'
RESET='\033[0m'
echo ""
echo "Deployment complete!" echo "Deployment complete!"
echo "App should be available at http://localhost:$PORT" echo ""
echo -e " App is running at: ${GREEN}http://localhost:${PORT}${RESET}"
echo ""
# Rainbow ASCII Art
RAINBOW=(
'\033[1;31m' # Red
'\033[1;33m' # Yellow
'\033[1;32m' # Green
'\033[1;36m' # Cyan
'\033[1;34m' # Blue
'\033[1;35m' # Magenta
)
ASCII=(
" __ ___ __ __ ____ __ "
" / |/ /___ ______/ /______/ /___ _ ______ / __ )/ /___ ____ _"
" / /|_/ / __ \`/ ___/ //_/ __ / __ \\ | /| / / __ \\/ __ / / __ \\/ __ \`/"
" / / / / /_/ / / / ,< / /_/ / /_/ / |/ |/ / / / / /_/ / / /_/ / /_/ / "
"/_/ /_/\\__,_/_/ /_/|_|\\__,_/\\____/|__/|__/_/ /_/_____/_/\\____/\\__, / "
" /____/ "
)
for i in "${!ASCII[@]}"; do
color="${RAINBOW[$((i % ${#RAINBOW[@]}))]}"
echo -e "${color}${ASCII[$i]}${RESET}"
done

View File

@@ -2,6 +2,13 @@ const { app, BrowserWindow } = require('electron');
const path = require('path'); const path = require('path');
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
/*
This will be discontinued in a bit.
Either move to Docker or get fucked.
*/
function createWindow() { function createWindow() {
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 1200, width: 1200,
@@ -14,7 +21,9 @@ function createWindow() {
// Load the Next.js app // Load the Next.js app
if (isDev) { if (isDev) {
mainWindow.loadURL('http://localhost:3000'); const baseUrl = process.env.BASE_URL || '';
const url = `http://localhost:3000${baseUrl}`;
mainWindow.loadURL(url);
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} else { } else {
mainWindow.loadFile(path.join(__dirname, '../.next/server/pages/index.html')); mainWindow.loadFile(path.join(__dirname, '../.next/server/pages/index.html'));

357
flowcharts/backend.drawio Normal file
View File

@@ -0,0 +1,357 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" version="27.2.0">
<diagram name="Markdown Backend Flowchart" id="eQoA7ipTtm_-JJKlHUD_">
<mxGraphModel dx="2374" dy="2271" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="KWtHj6Fe61qkRVlhPuuz-440" value="CLI Command" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontStyle=1;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="2040" y="900" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-441" value="Command Type" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="1855" y="900" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-442" value="get_all_posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2055" y="995" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-443" value="get_post_by_slug" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2045" y="1055" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-444" value="get_posts_by_tag" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2045" y="1115" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-445" value="watch_posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2060" y="1195" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-446" value="checkhealth" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2060" y="1255" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-447" value="force_reinterpret_all_posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2025" y="1315" width="150" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-539" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-448" target="KWtHj6Fe61qkRVlhPuuz-449">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-448" value="&lt;div&gt;Cache&lt;/div&gt;" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2250" y="995" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-540" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-449" target="KWtHj6Fe61qkRVlhPuuz-475">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-449" value="&lt;div&gt;Cache&lt;/div&gt;" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2250" y="1055" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-450" value="find_markdown_files" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2400" y="955" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-541" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-451" target="KWtHj6Fe61qkRVlhPuuz-453">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-451" value="get_posts_directory" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2400" y="1005" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-452" value="Scan for .md files" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2405" y="1085" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-542" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-453" target="KWtHj6Fe61qkRVlhPuuz-452">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-453" value="slug_to_path" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2415" y="1045" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-454" value="Read File Content" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2585" y="1045" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-455" value="File Size Check" style="rhombus;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2590" y="1095" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-543" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-456" target="KWtHj6Fe61qkRVlhPuuz-473">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-456" value="Parse Frontmatter" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2585" y="1135" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-457" value="gray_matter::parse" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2760" y="1135" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-458" value="process_anchor_links" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2925" y="1135" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-459" value="process_custom_tags" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2925" y="1185" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-460" value="Markdown Parser" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2765" y="1185" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-461" value="Parser::new_ext" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2770" y="1225" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-462" value="Event Processing Loop" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2920" y="1225" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-463" value="push_html" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2955" y="1275" width="70" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-464" value="AMMONIA Sanitization" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2920" y="1325" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-465" value="Create Post Struct" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2935" y="1365" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-545" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-466" target="KWtHj6Fe61qkRVlhPuuz-467">
<mxGeometry relative="1" as="geometry">
<mxPoint x="2810" y="1320" as="targetPoint" />
<Array as="points">
<mxPoint x="2830" y="1430" />
<mxPoint x="2830" y="1270" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-466" value="Final Post Object" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2935" y="1415" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-467" value="Insert into POST_CACHE" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2565" y="1255" width="150" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-468" value="Update POST_STATS" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2575" y="1295" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-469" value="Return Post" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2600" y="1335" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-470" value="Sort by Date" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2420" y="1125" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-471" value="Update ALL_POSTS_CACHE" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2375" y="1165" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-472" value="Return Posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2415" y="1205" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-547" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-473" target="KWtHj6Fe61qkRVlhPuuz-467">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-473" value="Error: File too large" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffebee;strokeColor=#c62828;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2580" y="1175" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-475" value="Filter by Tag" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2240" y="1125" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-476" value="Return Filtered Posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2215" y="1165" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-477" value="Create Watcher" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2400" y="1255" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-478" value="Watch Directory" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2400" y="1295" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-479" value="Clear Caches" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2405" y="1335" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-480" value="Check Posts Directory" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2205" y="1255" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-481" value="Generate HealthReport" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2200" y="1295" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-482" value="Clear All Caches" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2045" y="1385" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-483" value="Reprocess All Files" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2040" y="1425" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-484" value="Save to Disk" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="2060" y="1465" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-485" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-440" target="KWtHj6Fe61qkRVlhPuuz-441">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-486" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-442">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1910" y="1010" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-487" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-443">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1910" y="1010" />
<mxPoint x="2020" y="1070" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-488" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-444">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1910" y="1010" />
<mxPoint x="2030" y="1130" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-489" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-445">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1910" y="1010" />
<mxPoint x="2040" y="1210" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-490" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-446">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1910" y="1010" />
<mxPoint x="2040" y="1270" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-491" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-447">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="1910" y="1010" />
<mxPoint x="2000" y="1300" />
<mxPoint x="2020" y="1330" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-492" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-442" target="KWtHj6Fe61qkRVlhPuuz-448">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-493" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-443" target="KWtHj6Fe61qkRVlhPuuz-449">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-494" style="exitX=1.009;exitY=0.493;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-448" target="KWtHj6Fe61qkRVlhPuuz-450">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="2360" y="970" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-495" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-450" target="KWtHj6Fe61qkRVlhPuuz-451">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-497" style="exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-449" target="KWtHj6Fe61qkRVlhPuuz-453">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-498" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-453" target="KWtHj6Fe61qkRVlhPuuz-454">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-499" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-454" target="KWtHj6Fe61qkRVlhPuuz-455">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-500" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-455" target="KWtHj6Fe61qkRVlhPuuz-456">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-501" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-456" target="KWtHj6Fe61qkRVlhPuuz-457">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-502" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-457" target="KWtHj6Fe61qkRVlhPuuz-458">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-503" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-458" target="KWtHj6Fe61qkRVlhPuuz-459">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-504" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-459" target="KWtHj6Fe61qkRVlhPuuz-460">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-505" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-460" target="KWtHj6Fe61qkRVlhPuuz-461">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-506" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-461" target="KWtHj6Fe61qkRVlhPuuz-462">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-507" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-462" target="KWtHj6Fe61qkRVlhPuuz-463">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-508" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-463" target="KWtHj6Fe61qkRVlhPuuz-464">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-509" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-464" target="KWtHj6Fe61qkRVlhPuuz-465">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-510" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-465" target="KWtHj6Fe61qkRVlhPuuz-466">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-512" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-467" target="KWtHj6Fe61qkRVlhPuuz-468">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-513" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-468" target="KWtHj6Fe61qkRVlhPuuz-469">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-514" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-452" target="KWtHj6Fe61qkRVlhPuuz-470">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-515" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-470" target="KWtHj6Fe61qkRVlhPuuz-471">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-516" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-471" target="KWtHj6Fe61qkRVlhPuuz-472">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-519" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-444" target="KWtHj6Fe61qkRVlhPuuz-475">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-520" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-475" target="KWtHj6Fe61qkRVlhPuuz-476">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-521" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-445" target="KWtHj6Fe61qkRVlhPuuz-477">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="2270" y="1210" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-522" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-477" target="KWtHj6Fe61qkRVlhPuuz-478">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-523" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-478" target="KWtHj6Fe61qkRVlhPuuz-479">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-524" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-446" target="KWtHj6Fe61qkRVlhPuuz-480">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-525" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-480" target="KWtHj6Fe61qkRVlhPuuz-481">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-526" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-447" target="KWtHj6Fe61qkRVlhPuuz-482">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-527" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-482" target="KWtHj6Fe61qkRVlhPuuz-483">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-528" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-483" target="KWtHj6Fe61qkRVlhPuuz-484">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-531" value="Hit" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="2350" y="965" width="40" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-532" value="Miss" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="2280" y="1025" width="40" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-533" value="Hit" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="2350" y="1035" width="40" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-534" value="Miss" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="2270" y="1085" width="40" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-535" value="OK" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="2630" y="1065" width="40" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-536" value="Too Large" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="2625" y="1115" width="70" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-537" value="&amp;nbsp;" style="text;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2310" y="1300" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-546" value="&amp;nbsp;" style="text;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="2360" y="900" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-549" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-548" target="KWtHj6Fe61qkRVlhPuuz-440">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="KWtHj6Fe61qkRVlhPuuz-548" value="&lt;div&gt;Frontend API&lt;/div&gt;" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1">
<mxGeometry x="2035" y="740" width="120" height="60" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,266 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" version="27.2.0">
<diagram name="Docker Build Flowchart" id="docker-build-flow">
<mxGraphModel dx="1426" dy="795" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="start" value="Start Docker Build" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontStyle=1;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="40" y="40" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="docker-check" value="Check Docker Daemon" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="220" y="50" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="docker-error" value="Error: Docker daemon not running" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffebee;strokeColor=#c62828;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="220" y="120" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="cleanup-start" value="Cleanup Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontStyle=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="400" y="50" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="stop-containers" value="Stop &amp; Remove Containers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="400" y="110" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="remove-volume" value="Remove Docker Volume" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="400" y="160" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="build-start" value="Build Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontStyle=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="580" y="50" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="stage1-start" value="Stage 1: Rust Build" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="110" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="rust-base" value="FROM rust:latest" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="160" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="copy-rust" value="COPY ./markdown_backend" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="210" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="install-musl" value="Install musl-tools" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="260" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="cargo-build" value="cargo build --release" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="310" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="stage2-start" value="Stage 2: Node.js Build" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="360" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="node-base" value="FROM node:20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="410" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="copy-package" value="COPY package*.json" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="460" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="npm-install" value="RUN npm install" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="510" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="Baa9tTqOcb9ER4_EkQ3s-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" target="setup-start">
<mxGeometry relative="1" as="geometry">
<mxPoint x="700" y="573" as="sourcePoint" />
<mxPoint x="727.3399999999999" y="70" as="targetPoint" />
<Array as="points">
<mxPoint x="730" y="574" />
<mxPoint x="730" y="70" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="copy-source" value="COPY . ." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="580" y="560" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="setup-start" value="Setup Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontStyle=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="750" y="50" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="create-posts" value="Create /app/posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="110" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="copy-posts" value="COPY posts/*" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="160" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="copy-binary" value="COPY Rust Binary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="210" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="set-permissions" value="Set Permissions" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="260" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="npm-build" value="npm run build" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="310" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="create-docker-dir" value="Create /app/docker" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="360" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="set-env" value="ENV DOCKER_CONTAINER=true" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="410" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="expose-port" value="EXPOSE 3000" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="460" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="setup-entrypoint" value="Setup Entrypoint" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="750" y="510" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="build-complete" value="Build Complete" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="930" y="50" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="run-start" value="Run Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontStyle=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="930" y="110" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="docker-run" value="docker run" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="930" y="170" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="port-mapping" value="Port Mapping 8080:3000" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="930" y="220" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="volume-mount" value="Volume Mount" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="930" y="270" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="entrypoint-start" value="Entrypoint Execution" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="930" y="320" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="check-volume" value="Check Volume Empty?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="930" y="370" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="copy-builtin" value="Copy Built-in Posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="930" y="420" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="start-app" value="Start Application" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="930" y="470" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="container-running" value="Container Running" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="930" y="520" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="edge1" parent="1" source="start" target="docker-check" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge2" parent="1" source="docker-check" target="docker-error" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge3" parent="1" source="docker-check" target="cleanup-start" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge4" parent="1" source="cleanup-start" target="stop-containers" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge5" parent="1" source="stop-containers" target="remove-volume" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge6" parent="1" source="remove-volume" target="build-start" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="560" y="175" />
<mxPoint x="560" y="70" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge7" parent="1" source="build-start" target="stage1-start" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge8" parent="1" source="stage1-start" target="rust-base" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge9" parent="1" source="rust-base" target="copy-rust" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge10" parent="1" source="copy-rust" target="install-musl" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge11" parent="1" source="install-musl" target="cargo-build" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge13" parent="1" source="stage2-start" target="node-base" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge14" parent="1" source="node-base" target="copy-package" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge15" parent="1" source="copy-package" target="npm-install" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge16" parent="1" source="npm-install" target="copy-source" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge18" parent="1" source="setup-start" target="create-posts" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge19" parent="1" source="create-posts" target="copy-posts" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge20" parent="1" source="copy-posts" target="copy-binary" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge21" parent="1" source="copy-binary" target="set-permissions" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge22" parent="1" source="set-permissions" target="npm-build" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge23" parent="1" source="npm-build" target="create-docker-dir" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge24" parent="1" source="create-docker-dir" target="set-env" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge25" parent="1" source="set-env" target="expose-port" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge26" parent="1" source="expose-port" target="setup-entrypoint" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge27" parent="1" source="setup-entrypoint" target="build-complete" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="910" y="525" />
<mxPoint x="910" y="70" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="edge28" parent="1" source="build-complete" target="run-start" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge29" parent="1" source="run-start" target="docker-run" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge30" parent="1" source="docker-run" target="port-mapping" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge31" parent="1" source="port-mapping" target="volume-mount" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge32" parent="1" source="volume-mount" target="entrypoint-start" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge33" parent="1" source="entrypoint-start" target="check-volume" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge34" parent="1" source="check-volume" target="copy-builtin" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge36" parent="1" source="copy-builtin" target="start-app" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge37" parent="1" source="start-app" target="container-running" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="edge38" parent="1" source="container-running" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1000" y="580" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="label1" value="Running" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
<mxGeometry x="360" y="50" width="40" height="20" as="geometry" />
</mxCell>
<mxCell id="label2" value="Not Running" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
<mxGeometry x="290" y="90" width="50" height="20" as="geometry" />
</mxCell>
<mxCell id="label3" value="Empty" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
<mxGeometry x="1030" y="350" width="30" height="20" as="geometry" />
</mxCell>
<mxCell id="label4" value="Not Empty" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
<mxGeometry x="1030" y="400" width="40" height="20" as="geometry" />
</mxCell>
<mxCell id="Baa9tTqOcb9ER4_EkQ3s-2" value="Deployment Done." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="930" y="580" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="Baa9tTqOcb9ER4_EkQ3s-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0.093;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="cargo-build" target="stage2-start">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,7 +1,17 @@
#[warn(unused_imports)] #[warn(unused_imports)]
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
mod markdown; mod markdown;
use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts, get_parser_logs, clear_parser_logs, load_parser_logs_from_disk}; use markdown::{
get_all_posts,
get_post_by_slug,
get_posts_by_tag,
watch_posts,
get_parser_logs,
clear_parser_logs,
load_parser_logs_from_disk,
force_reinterpret_all_posts,
force_reparse_single_post
};
use serde_json; use serde_json;
use std::fs; use std::fs;
use std::io; use std::io;
@@ -16,7 +26,7 @@ use std::io::Read; // STD AYOOOOOOOOOOOOOO - Tsodin
#[derive(Parser)] #[derive(Parser)]
#[command(name = "Markdown Backend")] #[command(name = "Markdown Backend")]
#[command(about = "A CLI for managing markdown blog posts", long_about = None)] #[command(about = "Ein CLI für die Verwaltung von Markdown-Blogbeiträgen", long_about = None)]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
@@ -44,6 +54,12 @@ enum Commands {
Logs, Logs,
/// Clear parser logs /// Clear parser logs
ClearLogs, ClearLogs,
/// Force reinterpret all posts (clear cache and re-parse)
ReinterpretAll,
/// Force reparse a single post (clear cache and re-parse)
ReparsePost {
slug: String,
},
/// Parse markdown from file or stdin /// Parse markdown from file or stdin
Parse { Parse {
#[arg(long)] #[arg(long)]
@@ -87,9 +103,9 @@ fn main() {
println!("{}", serde_json::to_string(&posts).unwrap()); println!("{}", serde_json::to_string(&posts).unwrap());
} }
Commands::Watch => { Commands::Watch => {
println!("Watching for changes in posts directory. Press Ctrl+C to exit."); println!("Überwache Änderungen im Posts-Verzeichnis. Drücken Sie Strg+C zum Beenden.");
let _ = watch_posts(|| { let _ = watch_posts(|| {
println!("Posts directory changed!"); println!("Posts-Verzeichnis hat sich geändert!");
}); });
// Keep the main thread alive // Keep the main thread alive
loop { loop {
@@ -109,7 +125,36 @@ fn main() {
} }
Commands::ClearLogs => { Commands::ClearLogs => {
clear_parser_logs(); clear_parser_logs();
println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Logs cleared"})).unwrap()); println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Protokolle gelöscht"})).unwrap());
}
Commands::ReinterpretAll => {
match force_reinterpret_all_posts() {
Ok(posts) => {
println!("{}", serde_json::to_string(&serde_json::json!({
"success": true,
"message": format!("Alle Beiträge erfolgreich neu interpretiert. {} Beiträge verarbeitet.", posts.len())
})).unwrap());
}
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
}
}
Commands::ReparsePost { slug } => {
match force_reparse_single_post(slug) {
Ok(post) => {
println!("{}", serde_json::to_string(&serde_json::json!({
"success": true,
"message": format!("Beitrag '{}' erfolgreich neu geparst", slug),
"post": post
})).unwrap());
}
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
}
} }
Commands::Parse { file, stdin, ast } => { Commands::Parse { file, stdin, ast } => {
let content = if *stdin { let content = if *stdin {
@@ -119,7 +164,7 @@ fn main() {
} else if let Some(file_path) = file { } else if let Some(file_path) = file {
fs::read_to_string(file_path).unwrap() fs::read_to_string(file_path).unwrap()
} else { } else {
eprintln!("Either --file or --stdin must be specified"); eprintln!("Entweder --file oder --stdin muss angegeben werden");
std::process::exit(1); std::process::exit(1);
}; };

View File

@@ -180,6 +180,18 @@ static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
}); });
// Helper functions // Helper functions
fn ensure_cache_directory() {
let cache_dir = PathBuf::from("./cache");
if !cache_dir.exists() {
if let Err(e) = fs::create_dir_all(&cache_dir) {
eprintln!("Fehler beim Erstellen des Cache-Verzeichnisses: {}", e);
add_log("error", &format!("Fehler beim Erstellen des Cache-Verzeichnisses: {}", e), None, None);
} else {
add_log("info", "Cache-Verzeichnis erstellt: ./cache", None, None);
}
}
}
fn get_posts_directory() -> PathBuf { fn get_posts_directory() -> PathBuf {
let is_docker = std::env::var("DOCKER_CONTAINER").is_ok() let is_docker = std::env::var("DOCKER_CONTAINER").is_ok()
|| std::env::var("KUBERNETES_SERVICE_HOST").is_ok() || std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
@@ -207,29 +219,101 @@ fn get_posts_directory() -> PathBuf {
for candidate in candidates.iter() { for candidate in candidates.iter() {
let path = PathBuf::from(candidate); let path = PathBuf::from(candidate);
if path.exists() && path.is_dir() { if path.exists() && path.is_dir() {
add_log("info", &format!("Verwende Posts-Verzeichnis: {:?}", path), None, None);
return path; return path;
} }
} }
// Fallback: default to ./posts
PathBuf::from("./posts") // Fallback: create ./posts if it doesn't exist
let fallback_path = PathBuf::from("./posts");
if !fallback_path.exists() {
if let Err(e) = fs::create_dir_all(&fallback_path) {
add_log("error", &format!("Fehler beim Erstellen des Posts-Verzeichnisses: {}", e), None, None);
} else {
add_log("info", "Posts-Verzeichnis erstellt: ./posts", None, None);
}
}
fallback_path
} }
// Function to find Markdown files. // Function to find Markdown files with improved reliability
// This will scan Directories recursively
fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> { fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
let mut files = Vec::new(); let mut files = Vec::new();
if dir.is_dir() { let mut errors = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() { if !dir.exists() {
files.extend(find_markdown_files(&path)?); let error_msg = format!("Verzeichnis existiert nicht: {:?}", dir);
} else if path.extension().map(|e| e == "md").unwrap_or(false) { add_log("error", &error_msg, None, None);
files.push(path); return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg));
}
if !dir.is_dir() {
let error_msg = format!("Pfad ist kein Verzeichnis: {:?}", dir);
add_log("error", &error_msg, None, None);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, error_msg));
}
// Try to read directory with retry logic
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
add_log("error", &format!("Fehler beim Lesen des Verzeichnisses {:?}: {}", dir, e), None, None);
return Err(e);
}
};
for entry_result in entries {
match entry_result {
Ok(entry) => {
let path = entry.path();
// Skip hidden files and directories
if let Some(name) = path.file_name() {
if name.to_string_lossy().starts_with('.') {
continue;
}
}
if path.is_dir() {
// Recursively scan subdirectories
match find_markdown_files(&path) {
Ok(subfiles) => files.extend(subfiles),
Err(e) => {
let error_msg = format!("Fehler beim Scannen des Unterverzeichnisses {:?}: {}", path, e);
add_log("warning", &error_msg, None, None);
errors.push(error_msg);
}
}
} else if path.extension().map(|e| e == "md").unwrap_or(false) {
// Verify the file is readable
match fs::metadata(&path) {
Ok(metadata) => {
if metadata.is_file() {
files.push(path);
}
}
Err(e) => {
let error_msg = format!("Datei nicht zugänglich {:?}: {}", path, e);
add_log("warning", &error_msg, None, None);
errors.push(error_msg);
}
}
}
}
Err(e) => {
let error_msg = format!("Fehler beim Lesen des Verzeichniseintrags: {}", e);
add_log("warning", &error_msg, None, None);
errors.push(error_msg);
} }
} }
} }
// Log summary
add_log("info", &format!("{} Markdown-Dateien in {:?} gefunden", files.len(), dir), None, None);
if !errors.is_empty() {
add_log("warning", &format!("{} Fehler während der Verzeichnissuche aufgetreten", errors.len()), None, None);
}
Ok(files) Ok(files)
} }
@@ -312,11 +396,11 @@ fn process_custom_tags(content: &str) -> String {
// Handle simple tags without parameters // Handle simple tags without parameters
let simple_tags = [ let simple_tags = [
("<mytag />", "<div class=\"custom-tag mytag\">This is my custom tag content!</div>"), ("<mytag />", "<div class=\"custom-tag mytag\">Dies ist mein benutzerdefinierter Tag-Inhalt!</div>"),
("<warning />", "<div class=\"custom-tag warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warning: This is a custom warning tag!</div>"), ("<warning />", "<div class=\"custom-tag warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warnung: Dies ist ein benutzerdefiniertes Warnungs-Tag!</div>"),
("<info />", "<div class=\"custom-tag info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\"> Info: This is a custom info tag!</div>"), ("<info />", "<div class=\"custom-tag info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\"> Info: Dies ist ein benutzerdefiniertes Info-Tag!</div>"),
("<success />", "<div class=\"custom-tag success\" style=\"background: #d4edda; border: 1px solid #c3e6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">✅ Success: This is a custom success tag!</div>"), ("<success />", "<div class=\"custom-tag success\" style=\"background: #d4edda; border: 1px solid #c3e6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">✅ Erfolg: Dies ist ein benutzerdefiniertes Erfolgs-Tag!</div>"),
("<error />", "<div class=\"custom-tag error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Error: This is a custom error tag!</div>"), ("<error />", "<div class=\"custom-tag error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Fehler: Dies ist ein benutzerdefiniertes Fehler-Tag!</div>"),
]; ];
for (tag, replacement) in simple_tags.iter() { for (tag, replacement) in simple_tags.iter() {
@@ -331,18 +415,18 @@ fn process_custom_tags(content: &str) -> String {
match tag_name { match tag_name {
"mytag" => { "mytag" => {
format!("<div class=\"custom-tag mytag\" data-params=\"{}\">Custom content with params: {}</div>", params, params) format!("<div class=\"custom-tag mytag\" data-params=\"{}\">Benutzerdefinierter Inhalt mit Parametern: {}</div>", params, params)
}, },
"alert" => { "alert" => {
if params.contains("type=\"warning\"") { if params.contains("type=\"warning\"") {
"<div class=\"custom-tag alert warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warning Alert!</div>".to_string() "<div class=\"custom-tag alert warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warnungs-Alert!</div>".to_string()
} else if params.contains("type=\"error\"") { } else if params.contains("type=\"error\"") {
"<div class=\"custom-tag alert error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Error Alert!</div>".to_string() "<div class=\"custom-tag alert error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Fehler-Alert!</div>".to_string()
} else { } else {
"<div class=\"custom-tag alert info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\"> Info Alert!</div>".to_string() "<div class=\"custom-tag alert info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\"> Info-Alert!</div>".to_string()
} }
}, },
_ => format!("<div class=\"custom-tag {}\">Unknown custom tag: {}</div>", tag_name, tag_name) _ => format!("<div class=\"custom-tag {}\">Unbekanntes benutzerdefiniertes Tag: {}</div>", tag_name, tag_name)
} }
}).to_string(); }).to_string();
@@ -372,7 +456,7 @@ fn add_log(level: &str, message: &str, slug: Option<&str>, details: Option<&str>
} }
fn save_parser_logs_to_disk_inner(logs: &VecDeque<LogEntry>) -> std::io::Result<()> { fn save_parser_logs_to_disk_inner(logs: &VecDeque<LogEntry>) -> std::io::Result<()> {
let _ = std::fs::create_dir_all("./cache"); ensure_cache_directory();
let logs_vec: Vec<_> = logs.iter().cloned().collect(); let logs_vec: Vec<_> = logs.iter().cloned().collect();
let json = serde_json::to_string(&logs_vec)?; let json = serde_json::to_string(&logs_vec)?;
std::fs::write(PARSER_LOGS_PATH, json)?; std::fs::write(PARSER_LOGS_PATH, json)?;
@@ -406,7 +490,7 @@ pub fn rsparseinfo() -> String {
// This Function gets the Post by its Slugified Version. // This Function gets the Post by its Slugified Version.
// This is basically only used for Caching (loading from it). // This is basically only used for Caching (loading from it).
pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>> { pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
add_log("info", "Starting post parsing", Some(slug), None); add_log("info", "Starte Post-Parsing", Some(slug), None);
let mut sys = System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::everything()).with_cpu(CpuRefreshKind::everything())); let mut sys = System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::everything()).with_cpu(CpuRefreshKind::everything()));
sys.refresh_processes(); sys.refresh_processes();
@@ -428,7 +512,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
entry.last_cache_status = "hit".to_string(); entry.last_cache_status = "hit".to_string();
sys.refresh_process(pid); sys.refresh_process(pid);
entry.last_cpu_usage_percent = sys.process(pid).map(|p| p.cpu_usage()).unwrap_or(0.0) - before_cpu; entry.last_cpu_usage_percent = sys.process(pid).map(|p| p.cpu_usage()).unwrap_or(0.0) - before_cpu;
add_log("info", "Cache hit", Some(slug), None); add_log("info", "Cache-Treffer", Some(slug), None);
return Ok(post); return Ok(post);
} }
@@ -440,16 +524,16 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
let file_path = slug_to_path(slug, &posts_dir); let file_path = slug_to_path(slug, &posts_dir);
if !file_path.exists() { if !file_path.exists() {
let error_msg = format!("File not found: {:?}", file_path); let error_msg = format!("Datei nicht gefunden: {:?}", file_path);
add_log("error", &error_msg, Some(slug), None); add_log("error", &error_msg, Some(slug), None);
return Err(error_msg.into()); return Err(error_msg.into());
} }
let file_content = fs::read_to_string(&file_path)?; let file_content = fs::read_to_string(&file_path)?;
add_log("info", &format!("File loaded: {} bytes", file_content.len()), Some(slug), None); add_log("info", &format!("Datei geladen: {} Bytes", file_content.len()), Some(slug), None);
if file_content.len() > MAX_FILE_SIZE { if file_content.len() > MAX_FILE_SIZE {
let error_msg = format!("File too large: {} bytes (max: {} bytes)", file_content.len(), MAX_FILE_SIZE); let error_msg = format!("Datei zu groß: {} Bytes (max: {} Bytes)", file_content.len(), MAX_FILE_SIZE);
add_log("error", &error_msg, Some(slug), None); add_log("error", &error_msg, Some(slug), None);
return Err(error_msg.into()); return Err(error_msg.into());
} }
@@ -461,21 +545,21 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
match data.deserialize() { match data.deserialize() {
Ok(front) => front, Ok(front) => front,
Err(e) => { Err(e) => {
let error_msg = format!("Failed to deserialize frontmatter: {}", e); let error_msg = format!("Fehler beim Deserialisieren des Frontmatters: {}", e);
add_log("error", &error_msg, Some(slug), None); add_log("error", &error_msg, Some(slug), None);
return Err(error_msg.into()); return Err(error_msg.into());
} }
} }
} else { } else {
add_log("error", "No frontmatter found", Some(slug), None); add_log("error", "Kein Frontmatter gefunden", Some(slug), None);
return Err("No frontmatter found".into()); return Err("Kein Frontmatter gefunden".into());
}; };
let created_at = get_file_creation_date(&file_path)?; let created_at = get_file_creation_date(&file_path)?;
let processed_markdown = process_anchor_links(&result.content); let processed_markdown = process_anchor_links(&result.content);
let processed_markdown = process_custom_tags(&processed_markdown); let processed_markdown = process_custom_tags(&processed_markdown);
add_log("info", "Starting markdown parsing", Some(slug), Some(&format!("Content length: {} chars", processed_markdown.len()))); add_log("info", "Starte Markdown-Parsing", Some(slug), Some(&format!("Inhaltslänge: {} Zeichen", processed_markdown.len())));
let parser = Parser::new_ext(&processed_markdown, Options::all()); let parser = Parser::new_ext(&processed_markdown, Options::all());
let mut html_output = String::new(); let mut html_output = String::new();
@@ -496,8 +580,8 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
for event in parser { for event in parser {
event_count += 1; event_count += 1;
if start_parsing.elapsed().as_secs() > PARSING_TIMEOUT_SECS { if start_parsing.elapsed().as_secs() > PARSING_TIMEOUT_SECS {
let error_msg = "Parsing timeout - file too large"; let error_msg = "Parsing-Timeout - Datei zu groß";
add_log("error", error_msg, Some(slug), Some(&format!("Processed {} events", event_count))); add_log("error", error_msg, Some(slug), Some(&format!("{} Events verarbeitet", event_count)));
return Err(error_msg.into()); return Err(error_msg.into());
} }
@@ -550,7 +634,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
} }
} }
add_log("info", "Markdown parsing completed", Some(slug), Some(&format!("Processed {} events", event_count))); add_log("info", "Markdown-Parsing abgeschlossen", Some(slug), Some(&format!("{} Events verarbeitet", event_count)));
html::push_html(&mut html_output, events.into_iter()); html::push_html(&mut html_output, events.into_iter());
let sanitized_html = AMMONIA.clean(&html_output).to_string(); let sanitized_html = AMMONIA.clean(&html_output).to_string();
@@ -565,7 +649,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
summary: front.summary, summary: front.summary,
content: sanitized_html, content: sanitized_html,
created_at: created_at.to_rfc3339(), created_at: created_at.to_rfc3339(),
author: std::env::var("BLOG_OWNER").unwrap_or_else(|_| "Anonymous".to_string()), author: std::env::var("BLOG_OWNER").unwrap_or_else(|_| "Anonym".to_string()),
}; };
let compile_time = compile_start.elapsed(); let compile_time = compile_start.elapsed();
@@ -584,7 +668,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
sys.refresh_process(pid); sys.refresh_process(pid);
entry.last_cpu_usage_percent = sys.process(pid).map(|p| p.cpu_usage()).unwrap_or(0.0) - before_cpu; entry.last_cpu_usage_percent = sys.process(pid).map(|p| p.cpu_usage()).unwrap_or(0.0) - before_cpu;
add_log("info", "Post parsing completed successfully", Some(slug), Some(&format!("Interpret: {}ms, Compile: {}ms", interpret_time.as_millis(), compile_time.as_millis()))); add_log("info", "Post-Parsing erfolgreich abgeschlossen", Some(slug), Some(&format!("Interpretation: {}ms, Kompilierung: {}ms", interpret_time.as_millis(), compile_time.as_millis())));
Ok(post) Ok(post)
} }
@@ -631,7 +715,7 @@ pub fn watch_posts<F: Fn() + Send + 'static>(on_change: F) -> notify::Result<Rec
on_change(); on_change();
}, },
Err(e) => { Err(e) => {
eprintln!("watch error: {:?}", e); eprintln!("Überwachungsfehler: {:?}", e);
break; break;
} }
} }
@@ -654,12 +738,11 @@ pub fn load_post_cache_from_disk() {
} }
pub fn save_post_cache_to_disk() { pub fn save_post_cache_to_disk() {
ensure_cache_directory();
if let Ok(map) = serde_json::to_string(&*POST_CACHE.read().unwrap()) { if let Ok(map) = serde_json::to_string(&*POST_CACHE.read().unwrap()) {
let _ = fs::create_dir_all("./cache");
let _ = fs::write(POSTS_CACHE_PATH, map); let _ = fs::write(POSTS_CACHE_PATH, map);
} }
if let Ok(map) = serde_json::to_string(&*POST_STATS.read().unwrap()) { if let Ok(map) = serde_json::to_string(&*POST_STATS.read().unwrap()) {
let _ = fs::create_dir_all("./cache");
let _ = fs::write(POST_STATS_PATH, map); let _ = fs::write(POST_STATS_PATH, map);
} }
} }
@@ -677,10 +760,10 @@ pub fn checkhealth() -> HealthReport {
.filter(|e| e.path().extension().map(|ext| ext == "md").unwrap_or(false)) .filter(|e| e.path().extension().map(|ext| ext == "md").unwrap_or(false))
.count(); .count();
}, },
Err(e) => errors.push(format!("Failed to read posts dir: {}", e)), Err(e) => errors.push(format!("Fehler beim Lesen des Posts-Verzeichnisses: {}", e)),
} }
} else { } else {
errors.push("Posts directory does not exist".to_string()); errors.push("Posts-Verzeichnis existiert nicht".to_string());
} }
let cache_file_exists = Path::new(POSTS_CACHE_PATH).exists(); let cache_file_exists = Path::new(POSTS_CACHE_PATH).exists();
@@ -695,10 +778,10 @@ pub fn checkhealth() -> HealthReport {
cache_readable = true; cache_readable = true;
cache_post_count = Some(map.len()); cache_post_count = Some(map.len());
}, },
Err(e) => errors.push(format!("Cache file not valid JSON: {}", e)), Err(e) => errors.push(format!("Cache-Datei ist kein gültiges JSON: {}", e)),
} }
}, },
Err(e) => errors.push(format!("Failed to read cache file: {}", e)), Err(e) => errors.push(format!("Fehler beim Lesen der Cache-Datei: {}", e)),
} }
} }
@@ -711,10 +794,10 @@ pub fn checkhealth() -> HealthReport {
cache_stats_readable = true; cache_stats_readable = true;
cache_stats_count = Some(map.len()); cache_stats_count = Some(map.len());
}, },
Err(e) => errors.push(format!("Cache stats file not valid JSON: {}", e)), Err(e) => errors.push(format!("Cache-Statistik-Datei ist kein gültiges JSON: {}", e)),
} }
}, },
Err(e) => errors.push(format!("Failed to read cache stats file: {}", e)), Err(e) => errors.push(format!("Fehler beim Lesen der Cache-Statistik-Datei: {}", e)),
} }
} }
@@ -739,7 +822,90 @@ pub fn get_parser_logs() -> Vec<LogEntry> {
} }
pub fn clear_parser_logs() { pub fn clear_parser_logs() {
let mut logs = PARSER_LOGS.write().unwrap(); PARSER_LOGS.write().unwrap().clear();
logs.clear(); if let Err(e) = save_parser_logs_to_disk_inner(&VecDeque::new()) {
let _ = std::fs::remove_file(PARSER_LOGS_PATH); eprintln!("Fehler beim Speichern leerer Protokolle auf Festplatte: {}", e);
}
}
// Force reinterpret all posts by clearing cache and re-parsing
pub fn force_reinterpret_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> {
add_log("info", "Starte erzwungene Neuinterpretation aller Posts", None, None);
// Clear all caches
POST_CACHE.write().unwrap().clear();
ALL_POSTS_CACHE.write().unwrap().take();
POST_STATS.write().unwrap().clear();
add_log("info", "Alle Caches geleert", None, None);
// Get posts directory and find all markdown files
let posts_dir = get_posts_directory();
let markdown_files = find_markdown_files(&posts_dir)?;
add_log("info", &format!("{} Markdown-Dateien zur Neuinterpretation gefunden", markdown_files.len()), None, None);
let mut posts = Vec::new();
let mut success_count = 0;
let mut error_count = 0;
for file_path in markdown_files {
let slug = path_to_slug(&file_path, &posts_dir);
match get_post_by_slug(&slug) {
Ok(post) => {
posts.push(post);
success_count += 1;
add_log("info", &format!("Erfolgreich neuinterpretiert: {}", slug), Some(&slug), None);
}
Err(e) => {
error_count += 1;
add_log("error", &format!("Fehler bei der Neuinterpretation von {}: {}", slug, e), Some(&slug), None);
}
}
}
// Update the all posts cache
ALL_POSTS_CACHE.write().unwrap().replace(posts.clone());
// Save cache to disk
save_post_cache_to_disk();
add_log("info", &format!("Erzwungene Neuinterpretation abgeschlossen. Erfolgreich: {}, Fehler: {}", success_count, error_count), None, None);
Ok(posts)
}
// Force reparse a single post by clearing its cache and re-parsing
pub fn force_reparse_single_post(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
add_log("info", &format!("Starte erzwungenes Neuparsing des Posts: {}", slug), Some(slug), None);
// Clear this specific post from all caches
POST_CACHE.write().unwrap().remove(slug);
POST_STATS.write().unwrap().remove(slug);
// Clear the all posts cache since it might contain this post
ALL_POSTS_CACHE.write().unwrap().take();
add_log("info", &format!("Cache für Post geleert: {}", slug), Some(slug), None);
// Re-parse the post
let post = get_post_by_slug(slug)?;
// Update the all posts cache with the new post
let mut all_posts_cache = ALL_POSTS_CACHE.write().unwrap();
if let Some(ref mut posts) = *all_posts_cache {
// Remove old version if it exists
posts.retain(|p| p.slug != slug);
// Add new version
posts.push(post.clone());
// Sort by creation date
posts.sort_by(|a, b| b.created_at.cmp(&a.created_at));
}
// Save cache to disk
save_post_cache_to_disk();
add_log("info", &format!("Post erfolgreich neugeparst: {}", slug), Some(slug), None);
Ok(post)
} }

View File

@@ -1,101 +0,0 @@
project_setup:
description: Setup Rust and Next.js to compile Rust to WASM for web use
prerequisites:
- Node.js >= 18
- Rust >= 1.70
- wasm-pack installed (`cargo install wasm-pack`)
- Next.js app created (`npx create-next-app@latest`)
- Optional: TypeScript enabled
steps:
- name: Create Rust crate
run: |
mkdir rust-wasm
cd rust-wasm
cargo new --lib wasm_core
cd wasm_core
- name: Add wasm dependencies to Cargo.toml
file: rust-wasm/wasm_core/Cargo.toml
append:
dependencies:
wasm-bindgen: "0.2"
[lib]:
crate-type: ["cdylib"]
[package.metadata.wasm-pack.profile.release]
wasm-opt: true
- name: Write simple Rust function
file: rust-wasm/wasm_core/src/lib.rs
content: |
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
- name: Build WASM module with wasm-pack
run: |
cd rust-wasm/wasm_core
wasm-pack build --target web --out-dir ../pkg
nextjs_setup:
description: Integrate the WASM output into a Next.js app
steps:
- name: Move compiled WASM pkg to Next.js public or static
run: |
# Assuming your Next.js app is in ../my-app
mkdir -p ../my-app/public/wasm
cp -r ../rust-wasm/pkg/* ../my-app/public/wasm/
- name: Import and initialize WASM in React
file: my-app/app/page.tsx
content: |
'use client';
import { useEffect, useState } from "react";
export default function Home() {
const [output, setOutput] = useState("");
useEffect(() => {
(async () => {
const wasm = await import("../../public/wasm/wasm_core.js");
await wasm.default(); // init
const result = wasm.greet("Next.js + Rust");
setOutput(result);
})();
}, []);
return <div className="p-4 font-mono text-xl">{output}</div>;
}
- name: Add TypeScript support (optional)
tips:
- Type declarations are not emitted automatically by wasm-pack
- You can write your own `.d.ts` file for the exposed functions
example_file: my-app/types/wasm_core.d.ts
content: |
declare module "public/wasm/wasm_core.js" {
export function greet(name: string): string;
export default function init(): Promise<void>;
}
- name: Optimize WebAssembly (optional)
tips:
- Install `binaryen` to use `wasm-opt`
- Run `wasm-pack build --release` with `wasm-opt` enabled
- Produces smaller, faster WASM binaries
dev_commands:
- command: cargo install wasm-pack
description: Install wasm-pack for building to WebAssembly
- command: wasm-pack build --target web
description: Build the Rust crate into WASM + JS bindings for browser
- command: npm run dev
description: Start your Next.js frontend
optional_advanced:
- name: Use `wasm-bindgen` directly (without wasm-pack)
reason: More control over output but more setup required
- name: Use `next-transpile-modules` to load WASM via import from `pkg/`
reason: Allows more direct integration but may need webpack tuning

View File

@@ -7,6 +7,10 @@ const nextConfig = {
experimental: { experimental: {
serverComponentsExternalPackages: ['chokidar'] serverComponentsExternalPackages: ['chokidar']
}, },
basePath: process.env.BASE_URL || '',
env: {
NEXT_PUBLIC_BASE_URL: process.env.BASE_URL || '',
},
// Handle API routes that shouldn't be statically generated // Handle API routes that shouldn't be statically generated
async headers() { async headers() {
return [ return [

142
package-lock.json generated
View File

@@ -25,6 +25,7 @@
"emoji-picker-react": "^4.12.2", "emoji-picker-react": "^4.12.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"isomorphic-dompurify": "^2.25.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"marked": "^12.0.0", "marked": "^12.0.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
@@ -1212,19 +1213,6 @@
"parse5": "^7.0.0" "parse5": "^7.0.0"
} }
}, },
"node_modules/@types/jsdom/node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/@types/json5": { "node_modules/@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -5183,6 +5171,92 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/isomorphic-dompurify": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz",
"integrity": "sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==",
"license": "MIT",
"dependencies": {
"dompurify": "^3.2.6",
"jsdom": "^26.1.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/isomorphic-dompurify/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/isomorphic-dompurify/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/isomorphic-dompurify/node_modules/jsdom": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"license": "MIT",
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.5.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.1.1",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.1",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/isomorphic-dompurify/node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^6.1.32"
},
"engines": {
"node": ">=16"
}
},
"node_modules/iterator.prototype": { "node_modules/iterator.prototype": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
@@ -5327,18 +5401,6 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/jsdom/node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/jsdom/node_modules/rrweb-cssom": { "node_modules/jsdom/node_modules/rrweb-cssom": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -6314,6 +6376,18 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8246,6 +8320,24 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.86"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"license": "MIT"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@@ -28,6 +28,7 @@
"emoji-picker-react": "^4.12.2", "emoji-picker-react": "^4.12.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"isomorphic-dompurify": "^2.25.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"marked": "^12.0.0", "marked": "^12.0.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",

View File

@@ -5,26 +5,5 @@ tags: [about, profile]
author: rattatwinko author: rattatwinko
summary: This is the about page summary: This is the about page
--- ---
# About Me
Hi! I'm Rattatwinko, a passionate developer who loves building self-hosted tools and beautiful web experiences. _**config this in the monaco editor in the admin panel**_
![](assets/peta.png)
## Skills
- TypeScript, JavaScript
- Rust
- React, Next.js
- Tailwind CSS
- Docker
## Experience
- Indie developer, 2020present
- Open source contributor
## Projects
- **MarkdownBlog**: The site you're reading now! A fast, modern, and hackable markdown blog platform.
## Contact
- [GitHub](https://github.com/rattatwinko)
- [Email](mailto:me@example.com)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

View File

@@ -535,3 +535,9 @@ If you have seen this is not very mindfull of browser resources tho.
> *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer > *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer
> >
> <cite>— Rattatwinko, 2025 Q3</cite> > <cite>— Rattatwinko, 2025 Q3</cite>
## Hosting behind a subpath (nginx proxy)
If you want to serve your blog at a subpath (e.g. `/blog`), set `BASE_URL=/blog` in your `.env.local` file. All internal links and API calls will use this base path automatically.
Example: Your blog will be available at `http://localhost:3000/blog`

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import BadgeButton from './BadgeButton'; import BadgeButton from './BadgeButton';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { withBaseUrl } from '@/lib/baseUrl';
const InfoIcon = ( const InfoIcon = (
<svg width="16" height="16" fill="white" viewBox="0 0 16 16" aria-hidden="true"> <svg width="16" height="16" fill="white" viewBox="0 0 16 16" aria-hidden="true">
@@ -16,7 +17,7 @@ export default function AboutButton() {
label="ABOUT ME" label="ABOUT ME"
color="#2563eb" color="#2563eb"
icon={InfoIcon} icon={InfoIcon}
onClick={() => router.push('/posts/about')} onClick={() => router.push(withBaseUrl('/posts/about'))}
/> />
); );
} }

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import BadgeButton from './BadgeButton'; import BadgeButton from './BadgeButton';
import { withBaseUrl } from '@/lib/baseUrl';
const LockIcon = ( const LockIcon = (
<svg width="16" height="16" viewBox="0 0 20 20" fill="none"> <svg width="16" height="16" viewBox="0 0 20 20" fill="none">
@@ -19,7 +20,7 @@ export default function HeaderButtons() {
return ( return (
<div className="flex gap-2 justify-center sm:justify-end"> <div className="flex gap-2 justify-center sm:justify-end">
<a <a
href="/admin" href={withBaseUrl('/admin')}
target="_self" target="_self"
rel="noopener noreferrer" rel="noopener noreferrer"
className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded" className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
@@ -33,7 +34,7 @@ export default function HeaderButtons() {
/> />
</a> </a>
<a <a
href="/posts/about" href={withBaseUrl('/posts/about')}
target="_self" target="_self"
rel="noopener noreferrer" rel="noopener noreferrer"
className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded" className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"

View File

@@ -2,6 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { withBaseUrl } from '@/lib/baseUrl';
interface MobileNavProps { interface MobileNavProps {
blogOwner: string; blogOwner: string;
@@ -54,29 +55,17 @@ export default function MobileNav({ blogOwner }: MobileNavProps) {
<h2 className="text-lg font-bold mb-6">{blogOwner}&apos;s Blog</h2> <h2 className="text-lg font-bold mb-6">{blogOwner}&apos;s Blog</h2>
<nav className="space-y-4"> <nav className="space-y-4">
<Link <a href={withBaseUrl('/')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
href="/"
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
onClick={toggleMenu}
>
🏠 Home 🏠 Home
</Link> </a>
<Link <a href={withBaseUrl('/admin')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
href="/admin"
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
onClick={toggleMenu}
>
🔐 Admin 🔐 Admin
</Link> </a>
<Link <a href={withBaseUrl('/posts/about')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
href="/posts/about"
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
onClick={toggleMenu}
>
👤 About Me 👤 About Me
</Link> </a>
</nav> </nav>
<div className="mt-8 pt-6 border-t border-gray-200"> <div className="mt-8 pt-6 border-t border-gray-200">

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { withBaseUrl } from '@/lib/baseUrl';
interface Post { interface Post {
slug: string; slug: string;
@@ -23,7 +24,7 @@ export default function AboutPage() {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const response = await fetch("/api/posts/about"); const response = await fetch(withBaseUrl('/api/posts/about'));
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }

View File

@@ -0,0 +1,12 @@
import Editor from "@monaco-editor/react";
export default function MonacoEditorWrapper(props: any) {
return (
<Editor
height="600px"
defaultLanguage="markdown"
defaultValue={props.defaultValue || ""}
{...props}
/>
);
}

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import "@fontsource/jetbrains-mono"; import "@fontsource/jetbrains-mono";
import { marked } from "marked"; import { marked } from "marked";
import { withBaseUrl } from '@/lib/baseUrl';
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false }); const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
@@ -37,6 +39,27 @@ function stripFrontmatter(md: string): string {
return md; return md;
} }
// Helper to extract YAML frontmatter
function extractFrontmatter(md: string): { frontmatter: string; content: string } {
if (!md) return { frontmatter: '', content: '' };
if (md.startsWith('---')) {
const end = md.indexOf('---', 3);
if (end !== -1) {
const frontmatter = md.slice(0, end + 3);
const content = md.slice(end + 3).replace(/^\s+/, '');
return { frontmatter, content };
}
}
return { frontmatter: '', content: md };
}
// Helper to combine frontmatter and content
function combineFrontmatterAndContent(frontmatter: string, content: string): string {
if (!frontmatter) return content;
if (!content) return frontmatter;
return frontmatter + '\n\n' + content;
}
function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: { function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: {
nodes: Node[]; nodes: Node[];
onSelect: (slug: string) => void; onSelect: (slug: string) => void;
@@ -87,22 +110,76 @@ function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: {
export default function EditorPage() { export default function EditorPage() {
// State // State
const router = useRouter();
const [tree, setTree] = useState<Node[]>([]); const [tree, setTree] = useState<Node[]>([]);
const [selectedSlug, setSelectedSlug] = useState<string | null>(null); const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
const [fileContent, setFileContent] = useState<string>(""); const [fileContent, setFileContent] = useState<string>("");
const [originalContent, setOriginalContent] = useState<string>("");
const [fileTitle, setFileTitle] = useState<string>(""); const [fileTitle, setFileTitle] = useState<string>("");
const [vimMode, setVimMode] = useState(false); const [vimMode, setVimMode] = useState(false);
const [previewHtml, setPreviewHtml] = useState<string>(""); const [previewHtml, setPreviewHtml] = useState<string>("");
const [split, setSplit] = useState(50); // percent const [split, setSplit] = useState(50); // percent - default to 50/50 split
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [browserOpen, setBrowserOpen] = useState(true); const [browserOpen, setBrowserOpen] = useState(true);
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
const [pendingNavigation, setPendingNavigation] = useState<string | null>(null);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const monacoVimRef = useRef<any>(null); const monacoVimRef = useRef<any>(null);
// Check if there are unsaved changes
const hasUnsavedChanges = fileContent !== originalContent;
// Handle browser beforeunload event
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasUnsavedChanges]);
// Handle back navigation with unsaved changes check
const handleBackNavigation = () => {
if (hasUnsavedChanges) {
setShowUnsavedDialog(true);
setPendingNavigation('/admin');
} else {
router.push('/admin');
}
};
// Handle unsaved changes dialog actions
const handleUnsavedDialogAction = (action: 'save' | 'discard' | 'cancel') => {
if (action === 'save') {
handleSave().then(() => {
setOriginalContent(fileContent); // Reset unsaved state after save
setShowUnsavedDialog(false);
setPendingNavigation(null);
if (pendingNavigation) {
router.push(pendingNavigation);
}
});
} else if (action === 'discard') {
setFileContent(originalContent); // Revert to last saved
setShowUnsavedDialog(false);
setPendingNavigation(null);
if (pendingNavigation) {
router.push(pendingNavigation);
}
} else {
setShowUnsavedDialog(false);
setPendingNavigation(null);
}
};
// Fetch file tree // Fetch file tree
useEffect(() => { useEffect(() => {
fetch("/api/posts") fetch(withBaseUrl('/api/posts'))
.then(r => r.json()) .then(r => r.json())
.then(setTree); .then(setTree);
}, []); }, []);
@@ -111,10 +188,13 @@ export default function EditorPage() {
useEffect(() => { useEffect(() => {
if (!selectedSlug) return; if (!selectedSlug) return;
setLoading(true); setLoading(true);
fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`) fetch(withBaseUrl(`/api/posts/${encodeURIComponent(selectedSlug)}`))
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
setFileContent(stripFrontmatter(data.raw || data.content || "")); const { frontmatter, content } = extractFrontmatter(data.raw || data.content || "");
const combinedContent = combineFrontmatterAndContent(frontmatter, content);
setFileContent(combinedContent);
setOriginalContent(combinedContent);
setFileTitle(data.title || data.slug || ""); setFileTitle(data.title || data.slug || "");
setLoading(false); setLoading(false);
}); });
@@ -124,18 +204,40 @@ export default function EditorPage() {
async function handleSave() { async function handleSave() {
if (!selectedSlug) return; if (!selectedSlug) return;
setSaving(true); setSaving(true);
await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, {
method: "POST", try {
headers: { "Content-Type": "application/json" }, // First save the file
body: JSON.stringify({ markdown: fileContent }) const saveResponse = await fetch(withBaseUrl(`/api/posts/${encodeURIComponent(selectedSlug)}`), {
}); method: "POST",
setSaving(false); headers: { "Content-Type": "application/json" },
body: JSON.stringify({ markdown: fileContent })
});
if (!saveResponse.ok) {
throw new Error('Failed to save file');
}
// Then call Rust backend to reparse this specific post
const reparseResponse = await fetch(withBaseUrl(`/api/admin/posts?reparsePost=${encodeURIComponent(selectedSlug)}`));
if (!reparseResponse.ok) {
console.warn('Failed to reparse post, but file was saved');
} else {
console.log('Post saved and reparsed successfully');
}
setOriginalContent(fileContent); // Reset unsaved state after save
} catch (error) {
console.error('Error saving/reparsing post:', error);
} finally {
setSaving(false);
}
} }
// Live preview (JS markdown, not Rust) // Live preview (JS markdown, not Rust)
useEffect(() => { useEffect(() => {
if (!fileContent) { setPreviewHtml(""); return; } if (!fileContent) { setPreviewHtml(""); return; }
const html = typeof marked.parse === 'function' ? marked.parse(stripFrontmatter(fileContent)) : ''; const { content } = extractFrontmatter(fileContent);
const html = typeof marked.parse === 'function' ? marked.parse(content) : '';
if (typeof html === 'string') setPreviewHtml(html); if (typeof html === 'string') setPreviewHtml(html);
else if (html instanceof Promise) html.then(setPreviewHtml); else if (html instanceof Promise) html.then(setPreviewHtml);
else setPreviewHtml(''); else setPreviewHtml('');
@@ -144,6 +246,17 @@ export default function EditorPage() {
// Monaco Vim integration // Monaco Vim integration
async function handleEditorDidMount(editor: any, monaco: any) { async function handleEditorDidMount(editor: any, monaco: any) {
editorRef.current = editor; editorRef.current = editor;
// Ensure editor resizes properly
const resizeObserver = new ResizeObserver(() => {
editor.layout();
});
const editorContainer = editor.getContainerDomNode();
if (editorContainer) {
resizeObserver.observe(editorContainer);
}
if (vimMode) { if (vimMode) {
const { initVimMode } = await import("monaco-vim"); const { initVimMode } = await import("monaco-vim");
if (monacoVimRef.current) monacoVimRef.current.dispose(); if (monacoVimRef.current) monacoVimRef.current.dispose();
@@ -168,25 +281,57 @@ export default function EditorPage() {
// Split drag logic // Split drag logic
const dragRef = useRef(false); const dragRef = useRef(false);
function onDrag(e: React.MouseEvent) { const [isDragging, setIsDragging] = useState(false);
function onDrag(e: React.MouseEvent | MouseEvent) {
if (!dragRef.current) return; if (!dragRef.current) return;
const percent = (e.clientX / window.innerWidth) * 100; const percent = (e.clientX / window.innerWidth) * 100;
setSplit(Math.max(20, Math.min(80, percent))); setSplit(percent); // No min/max limits
} }
function onDragStart() { dragRef.current = true; document.body.style.cursor = "col-resize"; }
function onDragEnd() { dragRef.current = false; document.body.style.cursor = ""; } function onDragStart() {
dragRef.current = true;
setIsDragging(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}
function onDragEnd() {
dragRef.current = false;
setIsDragging(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
useEffect(() => { useEffect(() => {
function onMove(e: MouseEvent) { onDrag(e as any); } function onMove(e: MouseEvent) {
function onUp() { onDragEnd(); } if (dragRef.current) {
if (dragRef.current) { onDrag(e);
}
}
function onUp() {
if (dragRef.current) {
onDragEnd();
}
}
if (isDragging) {
window.addEventListener("mousemove", onMove); window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp); window.addEventListener("mouseup", onUp);
return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
};
} }
}, [dragRef.current]); }, [isDragging]);
// Layout logic for left pane (file browser + editor)
const leftPaneWidth = `${split}%`;
const fileBrowserWidth = 240;
// Only render MonacoEditor if the editor pane is visible and has width // Only render MonacoEditor if the editor pane is visible and has width
const showEditor = browserOpen ? true : split > 5; const showEditor = true; // Always show editor, it will resize based on container
return ( return (
<div className="h-screen w-screen bg-white flex flex-col font-mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}> <div className="h-screen w-screen bg-white flex flex-col font-mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}>
@@ -241,50 +386,101 @@ export default function EditorPage() {
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
<span className="text-black text-lg font-semibold">Markdown Editor</span> <span className="text-black text-lg font-semibold">Markdown Bearbeiter</span>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{/* Back Button */}
<button <button
onClick={handleSave} onClick={() => handleBackNavigation()}
className={`flex items-center gap-2 px-3 py-1 rounded transition-colors border border-blue-400 bg-blue-500 text-white hover:bg-blue-600 font-mono ${saving ? 'opacity-60 cursor-wait' : ''}`} className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border font-mono text-sm sm:text-base ${
saving
? 'opacity-60 cursor-wait border-red-400 bg-red-500 text-white'
: hasUnsavedChanges
? 'border-orange-400 bg-orange-500 text-white hover:bg-orange-600'
: 'border-red-400 bg-red-500 text-white hover:bg-red-600'
}`}
disabled={saving} disabled={saving}
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1" /></svg> <svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<span>Save</span> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
<span className="hidden sm:inline">
{hasUnsavedChanges ? 'Zurück*' : 'Zurück'}
</span>
</button> </button>
{/* Save Button */}
<button
onClick={handleSave}
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border font-mono text-sm sm:text-base ${
saving
? 'opacity-60 cursor-wait border-blue-400 bg-blue-500 text-white'
: hasUnsavedChanges
? 'border-orange-400 bg-orange-500 text-white hover:bg-orange-600'
: 'border-blue-400 bg-blue-500 text-white hover:bg-blue-600'
}`}
disabled={saving}
>
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1" /></svg>
<span className="hidden sm:inline">
{saving
? 'Am Speichern...'
: hasUnsavedChanges
? 'Speichern*'
: 'Speichern'
}
</span>
</button>
{/* Vim Mode Button */}
<button <button
onClick={() => setVimMode((v) => !v)} onClick={() => setVimMode((v) => !v)}
className={`flex items-center gap-2 px-3 py-1 rounded transition-colors border border-gray-300 ${vimMode ? "bg-green-600 text-white" : "bg-white text-gray-700 hover:bg-gray-100"}`} className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border border-gray-300 text-sm sm:text-base ${
vimMode
? "bg-green-600 text-white"
: "bg-white text-gray-700 hover:bg-gray-100"
}`}
> >
{/* Actual Vim SVG Icon */} {/* Actual Vim SVG Icon */}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-5 h-5" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor">
<title>vim</title> <title>vim</title>
<path d="M26.445 22.095l0.592-0.649h1.667l0.386 0.519-1.581 5.132h0.616l-0.1 0.261h-2.228l1.405-4.454h-2.518l-1.346 4.238h0.53l-0.091 0.217h-2.006l1.383-4.434h-2.619l-1.327 4.172h0.545l-0.090 0.261h-2.076l1.892-5.573h-0.732l0.114-0.339h2.062l0.649 0.671h1.132l0.614-0.692h1.326l0.611 0.669zM7.99 27.033h-2.141l-0.327-0.187v-21.979h-1.545l-0.125-0.125v-1.47l0.179-0.192h9.211l0.266 0.267v1.385l-0.177 0.216h-1.348v10.857l11.006-10.857h-2.607l-0.219-0.235v-1.453l0.151-0.139h9.36l0.165 0.166v1.337l-12.615 12.937h-0.466c-0.005-0-0.011-0-0.018-0-0.012 0-0.024 0.001-0.036 0.002l0.002-0-0.025 0.004c-0.058 0.012-0.108 0.039-0.149 0.075l0-0-0.429 0.369-0.005 0.004c-0.040 0.037-0.072 0.084-0.090 0.136l-0.001 0.002-0.37 1.037zM17.916 18.028l0.187 0.189-0.336 1.152-0.281 0.282h-1.211l-0.226-0.226 0.389-1.088 0.36-0.309zM13.298 27.42l1.973-5.635h-0.626l0.371-0.38h2.073l-1.953 5.692h0.779l-0.099 0.322zM30.996 15.982h-0.034l-5.396-5.396 5.377-5.516v-2.24l-0.811-0.81h-10.245l-0.825 0.756v1.306l-3.044-3.044v-0.034l-0.019 0.018-0.018-0.018v0.034l-1.612 1.613-0.672-0.673h-10.151l-0.797 0.865v2.356l0.77 0.77h0.9v6.636l-3.382 3.38h-0.034l0.018 0.016-0.018 0.017h0.034l3.382 3.382v8.081l1.133 0.654h2.902l2.321-2.379 5.206 5.206v0.035l0.019-0.017 0.017 0.017v-0.035l3.136-3.135h0.606c0.144-0.001 0.266-0.093 0.312-0.221l0.001-0.002 0.182-0.532c0.011-0.031 0.017-0.067 0.017-0.105 0-0.073-0.024-0.14-0.064-0.195l0.001 0.001 1.827-1.827-0.765 2.452c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.275 0.233 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.097 0.015 0 0 0 0 0 0h2.279c0.136-0.001 0.252-0.084 0.303-0.201l0.001-0.002 0.206-0.492c0.014-0.036 0.022-0.077 0.022-0.121 0-0.048-0.010-0.094-0.028-0.135l0.001 0.002c-0.035-0.082-0.1-0.145-0.18-0.177l-0.002-0.001c-0.036-0.015-0.077-0.024-0.121-0.025h-0.094l1.050-3.304h1.54l-1.27 4.025c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.274 0.232 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.098 0.015 0 0 0.001 0 0.001 0h2.502c0 0 0.001 0 0.001 0 0.14 0 0.26-0.087 0.308-0.21l0.001-0.002 0.205-0.535c0.013-0.034 0.020-0.073 0.020-0.114 0-0.142-0.090-0.264-0.215-0.311l-0.002-0.001c-0.034-0.013-0.073-0.021-0.114-0.021h-0.181l1.413-4.59c0.011-0.031 0.017-0.066 0.017-0.103 0-0.074-0.025-0.143-0.066-0.198l0.001 0.001-0.469-0.63-0.004-0.006c-0.061-0.078-0.156-0.127-0.261-0.127h-1.795c-0.093 0-0.177 0.039-0.237 0.101l-0 0-0.5 0.549h-0.78l-0.052-0.057 5.555-5.555h0.035l-0.017-0.014z"/> <path d="M26.445 22.095l0.592-0.649h1.667l0.386 0.519-1.581 5.132h0.616l-0.1 0.261h-2.228l1.405-4.454h-2.518l-1.346 4.238h0.53l-0.091 0.217h-2.006l1.383-4.434h-2.619l-1.327 4.172h0.545l-0.090 0.261h-2.076l1.892-5.573h-0.732l0.114-0.339h2.062l0.649 0.671h1.132l0.614-0.692h1.326l0.611 0.669zM7.99 27.033h-2.141l-0.327-0.187v-21.979h-1.545l-0.125-0.125v-1.47l0.179-0.192h9.211l0.266 0.267v1.385l-0.177 0.216h-1.348v10.857l11.006-10.857h-2.607l-0.219-0.235v-1.453l0.151-0.139h9.36l0.165 0.166v1.337l-12.615 12.937h-0.466c-0.005-0-0.011-0-0.018-0-0.012 0-0.024 0.001-0.036 0.002l0.002-0-0.025 0.004c-0.058 0.012-0.108 0.039-0.149 0.075l0-0-0.429 0.369-0.005 0.004c-0.040 0.037-0.072 0.084-0.090 0.136l-0.001 0.002-0.37 1.037zM17.916 18.028l0.187 0.189-0.336 1.152-0.281 0.282h-1.211l-0.226-0.226 0.389-1.088 0.36-0.309zM13.298 27.42l1.973-5.635h-0.626l0.371-0.38h2.073l-1.953 5.692h0.779l-0.099 0.322zM30.996 15.982h-0.034l-5.396-5.396 5.377-5.516v-2.24l-0.811-0.81h-10.245l-0.825 0.756v1.306l-3.044-3.044v-0.034l-0.019 0.018-0.018-0.018v0.034l-1.612 1.613-0.672-0.673h-10.151l-0.797 0.865v2.356l0.77 0.77h0.9v6.636l-3.382 3.38h-0.034l0.018 0.016-0.018 0.017h0.034l3.382 3.382v8.081l1.133 0.654h2.902l2.321-2.379 5.206 5.206v0.035l0.019-0.017 0.017 0.017v-0.035l3.136-3.135h0.606c0.144-0.001 0.266-0.093 0.312-0.221l0.001-0.002 0.182-0.532c0.011-0.031 0.017-0.067 0.017-0.105 0-0.073-0.024-0.14-0.064-0.195l0.001 0.001 1.827-1.827-0.765 2.452c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.275 0.233 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.097 0.015 0 0 0 0 0 0h2.279c0.136-0.001 0.252-0.084 0.303-0.201l0.001-0.002 0.206-0.492c0.014-0.036 0.022-0.077 0.022-0.121 0-0.048-0.010-0.094-0.028-0.135l0.001 0.002c-0.035-0.082-0.1-0.145-0.18-0.177l-0.002-0.001c-0.036-0.015-0.077-0.024-0.121-0.025h-0.094l1.050-3.304h1.54l-1.27 4.025c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.274 0.232 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.098 0.015 0 0 0.001 0 0.001 0h2.502c0 0 0.001 0 0.001 0 0.14 0 0.26-0.087 0.308-0.21l0.001-0.002 0.205-0.535c0.013-0.034 0.020-0.073 0.020-0.114 0-0.142-0.090-0.264-0.215-0.311l-0.002-0.001c-0.034-0.013-0.073-0.021-0.114-0.021h-0.181l1.413-4.59c0.011-0.031 0.017-0.066 0.017-0.103 0-0.074-0.025-0.143-0.066-0.198l0.001 0.001-0.469-0.63-0.004-0.006c-0.061-0.078-0.156-0.127-0.261-0.127h-1.795c-0.093 0-0.177 0.039-0.237 0.101l-0 0-0.5 0.549h-0.78l-0.052-0.057 5.555-5.555h0.035l-0.017-0.014z"/>
</svg> </svg>
<span className="hidden sm:inline">Vim Mode</span> <span className="hidden sm:inline">Vim Modus</span>
</button> </button>
</div> </div>
</div> </div>
{/* Split Layout */} {/* Split Layout */}
<div className="flex flex-1 min-h-0" style={{ userSelect: dragRef.current ? "none" : undefined }}> <div className="flex flex-1 min-h-0" style={{ userSelect: isDragging ? "none" : undefined }}>
{/* Left: File browser + Editor */} {/* Left: File browser + Editor */}
<div className="flex flex-col" style={{ width: browserOpen ? `${split}%` : '48px', minWidth: browserOpen ? 240 : 48, maxWidth: 900, background: "#fff" }}> <div className="flex flex-row h-full bg-white" style={{ width: leftPaneWidth, minWidth: 0, maxWidth: '100%' }}>
{/* File Browser Collapsible Toggle */} {/* File Browser Collapsible Toggle */}
<button <div style={{ width: 32, minWidth: 32, maxWidth: 32, display: 'flex', flexDirection: 'column' }}>
className="w-full flex items-center gap-2 px-2 py-1 bg-gray-100 border-b border-gray-200 text-gray-700 font-mono hover:bg-gray-200 focus:outline-none" <button
onClick={() => setBrowserOpen(o => !o)} className={`w-full flex items-center justify-center px-2 py-1 font-mono hover:bg-gray-200 focus:outline-none ${
style={{ fontFamily: 'JetBrains Mono, monospace' }} browserOpen
> ? 'bg-gray-100 border-b border-gray-200 text-gray-700'
<span className="text-lg">{browserOpen ? '▼' : '▶'}</span> : 'bg-gray-200 border-b border-gray-300 text-gray-600 hover:bg-gray-300'
<span className="font-bold text-sm">Files</span> }`}
</button> onClick={() => setBrowserOpen(o => !o)}
style={{ fontFamily: 'JetBrains Mono, monospace', height: 40 }}
title={browserOpen ? "Datei-Explorer ausblenden" : "Datei-Explorer anzeigen"}
>
<span className="text-lg" style={{ transform: browserOpen ? 'rotate(0deg)' : 'rotate(90deg)' }}>
</span>
</button>
</div>
{/* File browser content */}
{browserOpen && ( {browserOpen && (
<div className="h-48 border-b border-gray-200 p-2 overflow-auto bg-gray-50 text-gray-800 font-mono"> <div className="border-r border-gray-200 bg-gray-50 text-gray-800 font-mono overflow-auto" style={{ width: fileBrowserWidth, minWidth: fileBrowserWidth, maxWidth: fileBrowserWidth }}>
{tree.length === 0 ? ( <div className="h-64 p-2">
<div className="text-xs text-gray-400">No files found.</div> {tree.length === 0 ? (
) : ( <div className="text-xs text-gray-400">Keine Datein gefunden.</div>
<FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} /> ) : (
)} <FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} />
)}
</div>
</div> </div>
)} )}
{/* Monaco Editor */} {/* Monaco Editor */}
@@ -301,7 +497,7 @@ export default function EditorPage() {
fontFamily: 'JetBrains Mono', fontFamily: 'JetBrains Mono',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 15, fontSize: 15,
minimap: { enabled: false }, minimap: { enabled: true },
wordWrap: "on", wordWrap: "on",
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
smoothScrolling: true, smoothScrolling: true,
@@ -314,6 +510,16 @@ export default function EditorPage() {
cursorStyle: "line", cursorStyle: "line",
fixedOverflowWidgets: true, fixedOverflowWidgets: true,
readOnly: loading, readOnly: loading,
folding: true,
foldingStrategy: "indentation",
showFoldingControls: "always",
foldingHighlight: true,
foldingImportsByDefault: true,
unfoldOnClickAfterEndOfLine: false,
links: true,
colorDecorators: true,
formatOnPaste: true,
formatOnType: true,
}} }}
onChange={v => setFileContent(v ?? "")} onChange={v => setFileContent(v ?? "")}
onMount={handleEditorDidMount} onMount={handleEditorDidMount}
@@ -324,11 +530,18 @@ export default function EditorPage() {
</div> </div>
</div> </div>
</div> </div>
{/* Draggable Splitter */} {/* Draggable Splitter - always show */}
<div <div
className="w-2 cursor-col-resize bg-gray-200 hover:bg-gray-300 transition-colors" className={`w-1 cursor-col-resize transition-colors relative ${
isDragging ? 'bg-blue-500' : 'bg-gray-300 hover:bg-gray-400'
}`}
onMouseDown={onDragStart} onMouseDown={onDragStart}
/> >
{/* Drag handle indicator */}
<div className="absolute inset-y-0 left-1/2 transform -translate-x-1/2 w-4 flex items-center justify-center">
<div className="w-1 h-8 bg-gray-400 rounded-full opacity-50"></div>
</div>
</div>
{/* Right: Live Preview */} {/* Right: Live Preview */}
<div className="flex-1 bg-gray-50 p-8 overflow-auto border-l border-gray-200"> <div className="flex-1 bg-gray-50 p-8 overflow-auto border-l border-gray-200">
<article className="bg-white rounded-lg shadow-sm border p-6 sm:p-8"> <article className="bg-white rounded-lg shadow-sm border p-6 sm:p-8">
@@ -345,6 +558,45 @@ export default function EditorPage() {
</article> </article>
</div> </div>
</div> </div>
{/* Unsaved Changes Dialog */}
{showUnsavedDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Ungespeicherte Änderungen</h3>
</div>
<p className="text-gray-600 mb-6">
Sie haben ungespeicherte Änderungen. Möchten Sie diese speichern, bevor Sie fortfahren?
</p>
<div className="flex gap-3 justify-end">
<button
onClick={() => handleUnsavedDialogAction('cancel')}
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
Abbrechen
</button>
<button
onClick={() => handleUnsavedDialogAction('discard')}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
>
Verwerfen
</button>
<button
onClick={() => handleUnsavedDialogAction('save')}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Speichern
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }
@@ -363,7 +615,7 @@ class MonacoErrorBoundary extends React.Component<{children: React.ReactNode}, {
} }
render() { render() {
if (this.state.error) { if (this.state.error) {
return <div className="text-red-600 p-4">Editor error: {this.state.error.message}</div>; return <div className="text-red-600 p-4">Fehler: {this.state.error.message}</div>;
} }
return this.props.children; return this.props.children;
} }

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { withBaseUrl } from '@/lib/baseUrl';
interface Post { interface Post {
type: 'post'; type: 'post';
@@ -26,7 +27,7 @@ type Node = Post | Folder;
// Helper to get folder details // Helper to get folder details
async function getFolderDetails(path: string): Promise<{ created: string, items: number, size: number, error?: string }> { async function getFolderDetails(path: string): Promise<{ created: string, items: number, size: number, error?: string }> {
try { try {
const res = await fetch(`/api/admin/folders/details?path=${encodeURIComponent(path)}`); const res = await fetch(withBaseUrl(`/api/admin/folders/details?path=${encodeURIComponent(path)}`));
if (!res.ok) throw new Error('API error'); if (!res.ok) throw new Error('API error');
return await res.json(); return await res.json();
} catch (e) { } catch (e) {
@@ -38,7 +39,7 @@ async function getFolderDetails(path: string): Promise<{ created: string, items:
// Helper to get post size and creation date // Helper to get post size and creation date
async function getPostSize(slug: string): Promise<{ size: number | null, created: string | null }> { async function getPostSize(slug: string): Promise<{ size: number | null, created: string | null }> {
try { try {
const res = await fetch(`/api/admin/posts/size?slug=${encodeURIComponent(slug)}`); const res = await fetch(withBaseUrl(`/api/admin/posts/size?slug=${encodeURIComponent(slug)}`));
if (!res.ok) throw new Error('API error'); if (!res.ok) throw new Error('API error');
const data = await res.json(); const data = await res.json();
return { size: data.size, created: data.created }; return { size: data.size, created: data.created };
@@ -76,7 +77,7 @@ export default function ManagePage() {
const loadContent = async () => { const loadContent = async () => {
try { try {
const response = await fetch('/api/posts'); const response = await fetch(withBaseUrl('/api/posts'));
const data = await response.json(); const data = await response.json();
setNodes(data); setNodes(data);
} catch (error) { } catch (error) {
@@ -140,7 +141,7 @@ export default function ManagePage() {
type: deleteConfirm.item.type type: deleteConfirm.item.type
}); });
const response = await fetch('/api/admin/delete', { const response = await fetch(withBaseUrl('/api/admin/delete'), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -170,7 +171,7 @@ export default function ManagePage() {
// Move post API call // Move post API call
const movePost = async (post: Post, targetFolder: string[]) => { const movePost = async (post: Post, targetFolder: string[]) => {
try { try {
const response = await fetch('/api/admin/posts/move', { const response = await fetch(withBaseUrl('/api/admin/posts/move'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -239,12 +240,12 @@ export default function ManagePage() {
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6 sm:mb-8 space-y-4 sm:space-y-0"> <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6 sm:mb-8 space-y-4 sm:space-y-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4"> <div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
<h1 className="text-2xl sm:text-3xl font-bold">Inhaltsverwaltung</h1> <h1 className="text-2xl sm:text-3xl font-bold">Inhaltsverwaltung</h1>
<Link <a
href="/admin" href={withBaseUrl('/admin')}
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-base font-medium" className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-base font-medium"
> >
Zum Admin-Panel Zum Admin-Panel
</Link> </a>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@@ -461,7 +462,7 @@ export default function ManagePage() {
onClick={async () => { onClick={async () => {
if (!deleteAllConfirm.folder) return; if (!deleteAllConfirm.folder) return;
// Call delete API with recursive flag // Call delete API with recursive flag
await fetch('/api/admin/delete', { await fetch(withBaseUrl('/api/admin/delete'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { withBaseUrl } from '@/lib/baseUrl';
interface PostStats { interface PostStats {
slug: string; slug: string;
@@ -52,7 +53,7 @@ export default function RustStatusPage() {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const res = await fetch('/api/admin/posts?rsparseinfo=1'); const res = await fetch(withBaseUrl('/api/admin/posts?rsparseinfo=1'));
if (!res.ok) throw new Error('Fehler beim Laden der Statistiken'); if (!res.ok) throw new Error('Fehler beim Laden der Statistiken');
const data = await res.json(); const data = await res.json();
setStats(data); setStats(data);
@@ -67,7 +68,7 @@ export default function RustStatusPage() {
setHealthLoading(true); setHealthLoading(true);
setHealthError(null); setHealthError(null);
try { try {
const res = await fetch('/api/admin/posts?checkhealth=1'); const res = await fetch(withBaseUrl('/api/admin/posts?checkhealth=1'));
if (!res.ok) throw new Error('Fehler beim Laden des Health-Checks'); if (!res.ok) throw new Error('Fehler beim Laden des Health-Checks');
const data = await res.json(); const data = await res.json();
setHealth(data); setHealth(data);
@@ -82,7 +83,7 @@ export default function RustStatusPage() {
setLogsLoading(true); setLogsLoading(true);
setLogsError(null); setLogsError(null);
try { try {
const res = await fetch('/api/admin/posts?logs=1'); const res = await fetch(withBaseUrl('/api/admin/posts?logs=1'));
if (!res.ok) throw new Error('Fehler beim Laden der Logs'); if (!res.ok) throw new Error('Fehler beim Laden der Logs');
const data = await res.json(); const data = await res.json();
setLogs(data); setLogs(data);
@@ -95,7 +96,7 @@ export default function RustStatusPage() {
const clearLogs = async () => { const clearLogs = async () => {
try { try {
const res = await fetch('/api/admin/posts?clearLogs=1', { method: 'DELETE' }); const res = await fetch(withBaseUrl('/api/admin/posts?clearLogs=1'), { method: 'DELETE' });
if (!res.ok) throw new Error('Fehler beim Löschen der Logs'); if (!res.ok) throw new Error('Fehler beim Löschen der Logs');
await fetchLogs(); // Refresh logs after clearing await fetchLogs(); // Refresh logs after clearing
} catch (e: any) { } catch (e: any) {
@@ -103,6 +104,19 @@ export default function RustStatusPage() {
} }
}; };
const reinterpretAllPosts = async () => {
try {
const res = await fetch(withBaseUrl('/api/admin/posts?reinterpretAll=1'));
if (!res.ok) throw new Error('Fehler beim Neuinterpretieren der Posts');
const data = await res.json();
console.log('Neu-Interpretier Ergebins:', data);
// Refresh all data after reinterpret
await Promise.all([fetchStats(), fetchHealth(), fetchLogs()]);
} catch (e: any) {
console.error('Fehler beim Neu-Interpretieren => ', e);
}
};
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
fetchHealth(); fetchHealth();
@@ -176,7 +190,7 @@ export default function RustStatusPage() {
<div className="flex items-center gap-2 w-full sm:w-auto justify-end"> <div className="flex items-center gap-2 w-full sm:w-auto justify-end">
{/* Back to Admin button */} {/* Back to Admin button */}
<a <a
href="/admin" href={withBaseUrl('/admin')}
className="p-1.5 sm:px-3 sm:py-1.5 bg-gray-200 hover:bg-gray-300 rounded-lg shadow-sm flex items-center gap-1 transition-colors text-sm" className="p-1.5 sm:px-3 sm:py-1.5 bg-gray-200 hover:bg-gray-300 rounded-lg shadow-sm flex items-center gap-1 transition-colors text-sm"
title="Zurück zur Admin-Panel" title="Zurück zur Admin-Panel"
> >
@@ -358,12 +372,19 @@ export default function RustStatusPage() {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3">
<h2 className="text-base font-semibold">Parser Logs</h2> <h2 className="text-base font-semibold">Parser Logs</h2>
<div className="flex flex-col sm:flex-row gap-2"> <div className="flex flex-col sm:flex-row gap-2">
<button
onClick={reinterpretAllPosts}
className="px-2.5 py-1.5 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs transition-colors"
title="Neuinterpretation aller Beiträge erzwingen"
>
Alle Posts neu Interpretieren?
</button>
<button <button
onClick={clearLogs} onClick={clearLogs}
className="px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors" className="px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors"
title="Clear all logs" title="Logs Leeren"
> >
Clear Logs Logs Leeren
</button> </button>
</div> </div>
</div> </div>
@@ -385,10 +406,10 @@ export default function RustStatusPage() {
onChange={(e) => setLogFilter(e.target.value)} onChange={(e) => setLogFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm" className="px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
> >
<option value="all">All Levels</option> <option value="all">Alle Stufen</option>
<option value="info">Info</option> <option value="info">Info</option>
<option value="warning">Warning</option> <option value="warning">Warnungen</option>
<option value="error">Error</option> <option value="error">Fehler</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,17 @@
'use client'; 'use client';
export const dynamic = "force-dynamic";
/********************************************* /*********************************************
* This is the main admin page for the blog. * This is the main admin page for the blog.
* *
* Written Jun 19 2025 * Written Jun 19 2025
* Rewritten fucking 15 times cause of the
* fucking
* typescript linter.
*
* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault
*
* Push later when on local Network. (//5jul25) ## Already done
**********************************************/ **********************************************/
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
@@ -12,17 +20,22 @@ import Link from 'next/link';
import { marked } from 'marked'; import { marked } from 'marked';
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import matter from 'gray-matter'; import matter from 'gray-matter';
import dynamic from 'next/dynamic'; import dynamicImport from 'next/dynamic';
import { Theme } from 'emoji-picker-react'; import { Theme } from 'emoji-picker-react';
import '../highlight-github.css'; import '../highlight-github.css';
import MonacoEditor from '@monaco-editor/react'; import { withBaseUrl } from '@/lib/baseUrl';
import { initVimMode, VimMode } from 'monaco-vim'; const MonacoEditor = dynamicImport(() => import('./MonacoEditor'), { ssr: false });
// Import monaco-vim only on client side
let initVimMode: any = null;
let VimMode: any = null;
if (typeof window !== 'undefined') {
const monacoVim = require('monaco-vim');
initVimMode = monacoVim.initVimMode;
VimMode = monacoVim.VimMode;
}
import '@fontsource/jetbrains-mono'; import '@fontsource/jetbrains-mono';
// @ts-ignore-next-line
// eslint-disable-next-line
// If you want, you can move this to a global.d.ts file
// declare module 'monaco-vim';
interface Post { interface Post {
slug: string; slug: string;
@@ -55,7 +68,7 @@ interface Post {
type Node = Post | Folder; type Node = Post | Folder;
const EmojiPicker = dynamic(() => import('emoji-picker-react'), { ssr: false }); const EmojiPicker = dynamicImport(() => import('emoji-picker-react'), { ssr: false });
// Patch marked renderer to always add 'hljs' class to code blocks // Patch marked renderer to always add 'hljs' class to code blocks
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
@@ -89,12 +102,7 @@ export default function AdminPage() {
}); });
const [showManageContent, setShowManageContent] = useState(false); const [showManageContent, setShowManageContent] = useState(false);
const [managePath, setManagePath] = useState<string[]>([]); const [managePath, setManagePath] = useState<string[]>([]);
const [pinned, setPinned] = useState<string[]>(() => { const [pinned, setPinned] = useState<string[]>([]);
if (typeof window !== 'undefined') {
return JSON.parse(localStorage.getItem('pinnedPosts') || '[]');
}
return [];
});
const [pinFeedback, setPinFeedback] = useState<string | null>(null); const [pinFeedback, setPinFeedback] = useState<string | null>(null);
const [showChangePassword, setShowChangePassword] = useState(false); const [showChangePassword, setShowChangePassword] = useState(false);
const [changePwOld, setChangePwOld] = useState(''); const [changePwOld, setChangePwOld] = useState('');
@@ -104,12 +112,7 @@ export default function AdminPage() {
const [previewHtml, setPreviewHtml] = useState(''); const [previewHtml, setPreviewHtml] = useState('');
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null); const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
const [isDocker, setIsDocker] = useState<boolean>(false); const [isDocker, setIsDocker] = useState<boolean>(false);
const [rememberExportChoice, setRememberExportChoice] = useState<boolean>(() => { const [rememberExportChoice, setRememberExportChoice] = useState<boolean>(false);
if (typeof window !== 'undefined') {
return localStorage.getItem('rememberExportChoice') === 'true';
}
return false;
});
const [lastExportChoice, setLastExportChoice] = useState<string | null>(null); const [lastExportChoice, setLastExportChoice] = useState<string | null>(null);
const [emojiPickerOpen, setEmojiPickerOpen] = useState<string | null>(null); const [emojiPickerOpen, setEmojiPickerOpen] = useState<string | null>(null);
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null); const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
@@ -160,7 +163,7 @@ export default function AdminPage() {
useEffect(() => { useEffect(() => {
// Check if docker is used // Check if docker is used
fetch('/api/admin/docker') fetch(withBaseUrl('/api/admin/docker'))
.then(res => res.json()) .then(res => res.json())
.then(data => setIsDocker(!!data.docker)) .then(data => setIsDocker(!!data.docker))
.catch(() => setIsDocker(false)); .catch(() => setIsDocker(false));
@@ -168,7 +171,7 @@ export default function AdminPage() {
const loadContent = async () => { const loadContent = async () => {
try { try {
const response = await fetch('/api/posts'); const response = await fetch(withBaseUrl('/api/posts'));
const data = await response.json(); const data = await response.json();
setNodes(data); setNodes(data);
} catch (error) { } catch (error) {
@@ -187,7 +190,7 @@ export default function AdminPage() {
return; return;
} }
// Check password via API // Check password via API
const res = await fetch('/api/admin/password', { const res = await fetch(withBaseUrl('/api/admin/password'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pass, mode: 'login' }), body: JSON.stringify({ password: pass, mode: 'login' }),
@@ -211,7 +214,7 @@ export default function AdminPage() {
const handleCreatePost = async (e: React.FormEvent) => { const handleCreatePost = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
const response = await fetch('/api/admin/posts', { const response = await fetch(withBaseUrl('/api/admin/posts'), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -249,7 +252,7 @@ export default function AdminPage() {
} }
try { try {
const response = await fetch('/api/admin/folders', { const response = await fetch(withBaseUrl('/api/admin/folders'), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -345,7 +348,7 @@ export default function AdminPage() {
formData.append('file', file); formData.append('file', file);
formData.append('path', currentPath.join('/')); formData.append('path', currentPath.join('/'));
const response = await fetch('/api/admin/upload', { const response = await fetch(withBaseUrl('/api/admin/upload'), {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
@@ -379,7 +382,7 @@ export default function AdminPage() {
type: deleteConfirm.item.type type: deleteConfirm.item.type
}); });
const response = await fetch('/api/admin/delete', { const response = await fetch(withBaseUrl('/api/admin/delete'), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -412,7 +415,7 @@ export default function AdminPage() {
? prev.filter((s) => s !== slug) ? prev.filter((s) => s !== slug)
: [slug, ...prev]; : [slug, ...prev];
// Update pinned.json on the server // Update pinned.json on the server
fetch('/api/admin/posts', { fetch(withBaseUrl('/api/admin/posts'), {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pinned: newPinned }), body: JSON.stringify({ pinned: newPinned }),
@@ -451,7 +454,7 @@ export default function AdminPage() {
return; return;
} }
// Check old password // Check old password
const res = await fetch('/api/admin/password', { const res = await fetch(withBaseUrl('/api/admin/password'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: changePwOld, mode: 'login' }), body: JSON.stringify({ password: changePwOld, mode: 'login' }),
@@ -462,7 +465,7 @@ export default function AdminPage() {
return; return;
} }
// Set new password // Set new password
const res2 = await fetch('/api/admin/password', { const res2 = await fetch(withBaseUrl('/api/admin/password'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: changePwNew }), body: JSON.stringify({ password: changePwNew }),
@@ -482,7 +485,7 @@ export default function AdminPage() {
// Function to load a post's raw markdown // Function to load a post's raw markdown
const loadPostRaw = async (slug: string, folderPath: string) => { const loadPostRaw = async (slug: string, folderPath: string) => {
const params = new URLSearchParams({ slug, path: folderPath }); const params = new URLSearchParams({ slug, path: folderPath });
const res = await fetch(`/api/admin/posts/raw?${params.toString()}`); const res = await fetch(withBaseUrl(`/api/admin/posts/raw?${params.toString()}`));
if (!res.ok) { if (!res.ok) {
alert('Error loading post'); alert('Error loading post');
return; return;
@@ -514,7 +517,7 @@ export default function AdminPage() {
summary: newPost.summary, summary: newPost.summary,
author: process.env.NEXT_PUBLIC_BLOG_OWNER + "'s" || 'Anonymous', author: process.env.NEXT_PUBLIC_BLOG_OWNER + "'s" || 'Anonymous',
}); });
const response = await fetch('/api/admin/posts', { const response = await fetch(withBaseUrl('/api/admin/posts'), {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -536,15 +539,7 @@ export default function AdminPage() {
}; };
function handleExportTarball() { function handleExportTarball() {
// Check if we should use the remembered choice if (typeof window === 'undefined') return;
if (rememberExportChoice && lastExportChoice) {
if (lastExportChoice === 'docker') {
exportFromEndpoint('/api/admin/export');
} else if (lastExportChoice === 'local') {
exportFromEndpoint('/api/admin/exportlocal');
}
return;
}
// Create popup modal // Create popup modal
const modal = document.createElement('div'); const modal = document.createElement('div');
@@ -612,9 +607,9 @@ export default function AdminPage() {
} }
closeModal(); closeModal();
if (choice === 'docker') { if (choice === 'docker') {
exportFromEndpoint('/api/admin/export'); exportFromEndpoint(withBaseUrl('/api/admin/export'));
} else if (choice === 'local') { } else if (choice === 'local') {
exportFromEndpoint('/api/admin/exportlocal'); exportFromEndpoint(withBaseUrl('/api/admin/exportlocal'));
} }
}; };
@@ -637,6 +632,7 @@ export default function AdminPage() {
} }
function exportFromEndpoint(endpoint: string) { function exportFromEndpoint(endpoint: string) {
if (typeof window === 'undefined') return;
fetch(endpoint) fetch(endpoint)
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw new Error('Export failed'); if (!res.ok) throw new Error('Export failed');
@@ -660,6 +656,15 @@ export default function AdminPage() {
setLastExportChoice(null); setLastExportChoice(null);
}; };
// Hydrate pinned, rememberExportChoice, lastExportChoice from localStorage on client only
useEffect(() => {
if (typeof window !== 'undefined') {
setPinned(JSON.parse(localStorage.getItem('pinnedPosts') || '[]'));
setRememberExportChoice(localStorage.getItem('rememberExportChoice') === 'true');
setLastExportChoice(localStorage.getItem('lastExportChoice'));
}
}, []);
// Simple and reliable emoji update handler // Simple and reliable emoji update handler
const handleSetFolderEmoji = async (folderPath: string, emoji: string) => { const handleSetFolderEmoji = async (folderPath: string, emoji: string) => {
try { try {
@@ -689,7 +694,7 @@ export default function AdminPage() {
// Save to JSON file in background // Save to JSON file in background
try { try {
console.log('Fetching current pinned data...'); console.log('Fetching current pinned data...');
const pinnedRes = await fetch('/api/admin/posts', { method: 'GET' }); const pinnedRes = await fetch(withBaseUrl('/api/admin/posts'), { method: 'GET' });
if (!pinnedRes.ok) { if (!pinnedRes.ok) {
throw new Error(`Failed to fetch pinned data: ${pinnedRes.status}`); throw new Error(`Failed to fetch pinned data: ${pinnedRes.status}`);
} }
@@ -702,7 +707,7 @@ export default function AdminPage() {
console.log('Updated folderEmojis:', folderEmojis); console.log('Updated folderEmojis:', folderEmojis);
console.log('Saving to pinned.json...'); console.log('Saving to pinned.json...');
const saveRes = await fetch('/api/admin/posts', { const saveRes = await fetch(withBaseUrl('/api/admin/posts'), {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folderEmojis, pinned: pinnedData.pinned || [] }), body: JSON.stringify({ folderEmojis, pinned: pinnedData.pinned || [] }),
@@ -772,7 +777,7 @@ export default function AdminPage() {
// Attach/detach Vim mode when vimMode changes // Attach/detach Vim mode when vimMode changes
useEffect(() => { useEffect(() => {
if (vimMode && monacoRef.current) { if (vimMode && monacoRef.current && initVimMode) {
// @ts-ignore // @ts-ignore
vimInstanceRef.current = initVimMode(monacoRef.current, vimStatusRef.current); vimInstanceRef.current = initVimMode(monacoRef.current, vimStatusRef.current);
} else if (vimInstanceRef.current) { } else if (vimInstanceRef.current) {
@@ -882,7 +887,7 @@ export default function AdminPage() {
</span> </span>
</button> </button>
<a <a
href="/admin/manage/rust-status" href={withBaseUrl('/admin/manage/rust-status')}
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-teal-500 to-blue-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-teal-600 hover:to-blue-600 transition-all focus:outline-none focus:ring-2 focus:ring-teal-400" className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-teal-500 to-blue-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-teal-600 hover:to-blue-600 transition-all focus:outline-none focus:ring-2 focus:ring-teal-400"
title="View Rust Parser Dashboard" title="View Rust Parser Dashboard"
style={{ minWidth: '160px' }} style={{ minWidth: '160px' }}
@@ -898,9 +903,9 @@ export default function AdminPage() {
</a> </a>
{/* VS Code Editor Button */} {/* VS Code Editor Button */}
<a <a
href="/admin/editor" href={withBaseUrl('/admin/editor')}
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-gray-700 to-blue-700 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-gray-800 hover:to-blue-800 transition-all focus:outline-none focus:ring-2 focus:ring-blue-400" className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-gray-700 to-blue-700 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-gray-800 hover:to-blue-800 transition-all focus:outline-none focus:ring-2 focus:ring-blue-400"
title="Markdown Editor (VS Code Style)" title="Markdown Bearbeiter"
style={{ minWidth: '160px' }} style={{ minWidth: '160px' }}
> >
{/* VS Code SVG Icon */} {/* VS Code SVG Icon */}
@@ -917,7 +922,7 @@ export default function AdminPage() {
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/> <path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
</g> </g>
<g style={{ mixBlendMode: 'overlay' }} opacity="0.25"> <g style={{ mixBlendMode: 'overlay' }} opacity="0.25">
<path fillRule="evenodd" clipRule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/> <path fillRule="evenodd" clipRule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.29775 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
</g> </g>
</g> </g>
<defs> <defs>
@@ -946,8 +951,8 @@ export default function AdminPage() {
</defs> </defs>
</svg> </svg>
<span className="flex flex-col items-start"> <span className="flex flex-col items-start">
<span>Editor</span> <span>Markdown Editor</span>
<span className="text-xs font-normal text-blue-100">VS Code</span> <span className="text-xs font-normal text-blue-100">Visual Studio Code</span>
</span> </span>
</a> </a>
{rememberExportChoice && lastExportChoice && ( {rememberExportChoice && lastExportChoice && (
@@ -1182,7 +1187,7 @@ export default function AdminPage() {
height="100%" height="100%"
defaultLanguage="markdown" defaultLanguage="markdown"
value={newPost.content} value={newPost.content}
onChange={(value) => setNewPost({ ...newPost, content: value || '' })} onChange={(value?: string) => setNewPost({ ...newPost, content: value || '' })}
options={{ options={{
minimap: { enabled: false }, minimap: { enabled: false },
wordWrap: 'on', wordWrap: 'on',
@@ -1193,7 +1198,7 @@ export default function AdminPage() {
automaticLayout: true, automaticLayout: true,
fontFamily: 'JetBrains Mono, monospace', fontFamily: 'JetBrains Mono, monospace',
}} }}
onMount={(editor) => { onMount={(editor: any) => {
monacoRef.current = editor; monacoRef.current = editor;
}} }}
/> />
@@ -1473,7 +1478,7 @@ export default function AdminPage() {
</div> </div>
))} ))}
</div> </div>
<a href="/admin/manage" className="block mt-6 text-blue-600 hover:underline text-sm sm:text-base">Zur Inhaltsverwaltung</a> <a href={withBaseUrl('/admin/manage')} className="block mt-6 text-blue-600 hover:underline text-sm sm:text-base">Zur Inhaltsverwaltung</a>
</div> </div>
)} )}
</div> </div>

View File

@@ -110,6 +110,46 @@ export async function GET(request: Request) {
}); });
} }
} }
const reinterpretAll = searchParams.get('reinterpretAll');
if (reinterpretAll === '1') {
// Call the Rust backend to force reinterpret all posts
const rustResult = spawnSync(
process.cwd() + '/markdown_backend/target/release/markdown_backend',
['reinterpret-all'],
{ encoding: 'utf-8' }
);
if (rustResult.status === 0 && rustResult.stdout) {
return new Response(rustResult.stdout, {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} else {
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
const reparsePost = searchParams.get('reparsePost');
if (reparsePost) {
// Call the Rust backend to reparse a specific post
const rustResult = spawnSync(
process.cwd() + '/markdown_backend/target/release/markdown_backend',
['reparse-post', reparsePost],
{ encoding: 'utf-8' }
);
if (rustResult.status === 0 && rustResult.stdout) {
return new Response(rustResult.stdout, {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} else {
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
// Return the current pinned.json object // Return the current pinned.json object
try { try {
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json'); const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');

View File

@@ -5,8 +5,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import matter from 'gray-matter'; import matter from 'gray-matter';
import { marked } from 'marked'; import { marked } from 'marked';
import DOMPurify from 'dompurify'; import createDOMPurify from 'isomorphic-dompurify';
import { JSDOM } from 'jsdom';
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import { getPostsDirectory } from '@/lib/postsDirectory'; import { getPostsDirectory } from '@/lib/postsDirectory';
@@ -106,10 +105,8 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi
let processedContent = ''; let processedContent = '';
try { try {
const rawHtml = marked.parse(content); const rawHtml = marked.parse(content) as string;
const window = new JSDOM('').window; processedContent = createDOMPurify.sanitize(rawHtml, {
const purify = DOMPurify(window);
processedContent = purify.sanitize(rawHtml as string, {
ALLOWED_TAGS: [ ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'a', 'ul', 'ol', 'li', 'blockquote', 'p', 'a', 'ul', 'ol', 'li', 'blockquote',
@@ -123,7 +120,7 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi
'src', 'alt', 'title', 'width', 'height', 'src', 'alt', 'title', 'width', 'height',
'frameborder', 'allowfullscreen' 'frameborder', 'allowfullscreen'
], ],
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+\.\-]+(?:[^a-z+\.\-:]|$))/i
}); });
} catch (err) { } catch (err) {
console.error(`Error processing markdown for ${relPath}:`, err); console.error(`Error processing markdown for ${relPath}:`, err);

View File

@@ -7,6 +7,7 @@ import AboutButton from './AboutButton';
import BadgeButton from './BadgeButton'; import BadgeButton from './BadgeButton';
import HeaderButtons from './HeaderButtons'; import HeaderButtons from './HeaderButtons';
import MobileNav from './MobileNav'; import MobileNav from './MobileNav';
import { withBaseUrl } from '@/lib/baseUrl';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
@@ -46,11 +47,11 @@ export default function RootLayout({
return ( return (
<html lang="de" className="h-full"> <html lang="de" className="h-full">
<Head> <Head>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href={withBaseUrl('/favicon.ico')} />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href={withBaseUrl('/apple-touch-icon.png')} />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href={withBaseUrl('/favicon-32x32.png')} />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href={withBaseUrl('/favicon-16x16.png')} />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href={withBaseUrl('/site.webmanifest')} />
</Head> </Head>
<body className={`${inter.className} h-full min-h-screen flex flex-col`}> <body className={`${inter.className} h-full min-h-screen flex flex-col`}>
<MobileNav blogOwner={blogOwner} /> <MobileNav blogOwner={blogOwner} />

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { format } from 'date-fns'; import { format } from 'date-fns';
import React from 'react'; import React from 'react';
import { withBaseUrl } from '@/lib/baseUrl';
interface Post { interface Post {
type: 'post'; type: 'post';
@@ -47,7 +48,7 @@ export default function Home() {
const setupSSE = () => { const setupSSE = () => {
try { try {
eventSource = new EventSource('/api/posts/stream'); eventSource = new EventSource(withBaseUrl('/api/posts/stream'));
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
@@ -101,7 +102,7 @@ export default function Home() {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
const response = await fetch('/api/posts'); const response = await fetch(withBaseUrl('/api/posts'));
if (!response.ok) { if (!response.ok) {
throw new Error(`API error: ${response.status}`); throw new Error(`API error: ${response.status}`);
} }

View File

@@ -4,6 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { withBaseUrl } from '@/lib/baseUrl';
interface Post { interface Post {
slug: string; slug: string;
@@ -55,7 +56,7 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
const setupSSE = () => { const setupSSE = () => {
try { try {
eventSource = new EventSource('/api/posts/stream'); eventSource = new EventSource(withBaseUrl('/api/posts/stream'));
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
@@ -109,7 +110,7 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const response = await fetch(`/api/posts/${encodeURIComponent(slugPath)}`); const response = await fetch(withBaseUrl(`/api/posts/${encodeURIComponent(slugPath)}`));
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }

16
src/lib/baseUrl.ts Normal file
View File

@@ -0,0 +1,16 @@
declare const process: { env: { NEXT_PUBLIC_BASE_URL?: string } };
export function withBaseUrl(path: string): string {
let base = '';
if (typeof process !== 'undefined' && process.env && process.env.NEXT_PUBLIC_BASE_URL) {
base = process.env.NEXT_PUBLIC_BASE_URL;
}
if (!base || base === '/') return path;
// Ensure base starts with / and does not end with /
if (!base.startsWith('/')) base = '/' + base;
if (base.endsWith('/')) base = base.slice(0, -1);
// Ensure path starts with /
if (!path.startsWith('/')) path = '/' + path;
// Avoid double slashes
return base + path;
}

15
tooling/ascii.py Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/python
from pyfiglet import Figlet
import sys
def asciiart(text: str) -> str:
f = Figlet(font="slant")
return f.renderText(text)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python ascii.py \"TEXT\"")
sys.exit(1)
text = sys.argv[1]
print(asciiart(text))