7 Commits
vim ... subpath

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
27 changed files with 916 additions and 285 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 #
@@ -8,4 +11,5 @@ NEXT_SOCIAL_TWITTER="https://twitter.com/user" # I dont h
NEXT_SOCIAL_GITHUB_STATE="true" # I Have GitHub so this is True (if you dont then set this to false) # NEXT_SOCIAL_GITHUB_STATE="true" # I Have GitHub so this is True (if you dont then set this to false) #
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

View File

@@ -500,4 +500,26 @@ For issues and questions, please check the project structure and API documentati
- **🔄 Force Reparse Button**: One-click cache clearing and post reparsing - **🔄 Force Reparse Button**: One-click cache clearing and post reparsing
- **📊 Enhanced Rust Status**: Real-time parser performance monitoring - **📊 Enhanced Rust Status**: Real-time parser performance monitoring
- **🔍 Improved Log Management**: Better filtering and search capabilities - **🔍 Improved Log Management**: Better filtering and search capabilities
- **📁 Directory Health Monitoring**: Comprehensive file system diagnostics - **📁 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

@@ -26,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,
@@ -103,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 {
@@ -125,14 +125,14 @@ 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 => { Commands::ReinterpretAll => {
match force_reinterpret_all_posts() { match force_reinterpret_all_posts() {
Ok(posts) => { Ok(posts) => {
println!("{}", serde_json::to_string(&serde_json::json!({ println!("{}", serde_json::to_string(&serde_json::json!({
"success": true, "success": true,
"message": format!("All posts reinterpreted successfully. Processed {} posts.", posts.len()) "message": format!("Alle Beiträge erfolgreich neu interpretiert. {} Beiträge verarbeitet.", posts.len())
})).unwrap()); })).unwrap());
} }
Err(e) => { Err(e) => {
@@ -146,7 +146,7 @@ fn main() {
Ok(post) => { Ok(post) => {
println!("{}", serde_json::to_string(&serde_json::json!({ println!("{}", serde_json::to_string(&serde_json::json!({
"success": true, "success": true,
"message": format!("Post '{}' reparsed successfully", slug), "message": format!("Beitrag '{}' erfolgreich neu geparst", slug),
"post": post "post": post
})).unwrap()); })).unwrap());
} }
@@ -164,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

@@ -184,10 +184,10 @@ fn ensure_cache_directory() {
let cache_dir = PathBuf::from("./cache"); let cache_dir = PathBuf::from("./cache");
if !cache_dir.exists() { if !cache_dir.exists() {
if let Err(e) = fs::create_dir_all(&cache_dir) { if let Err(e) = fs::create_dir_all(&cache_dir) {
eprintln!("Failed to create cache directory: {}", e); eprintln!("Fehler beim Erstellen des Cache-Verzeichnisses: {}", e);
add_log("error", &format!("Failed to create cache directory: {}", e), None, None); add_log("error", &format!("Fehler beim Erstellen des Cache-Verzeichnisses: {}", e), None, None);
} else { } else {
add_log("info", "Created cache directory: ./cache", None, None); add_log("info", "Cache-Verzeichnis erstellt: ./cache", None, None);
} }
} }
} }
@@ -219,7 +219,7 @@ 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!("Using posts directory: {:?}", path), None, None); add_log("info", &format!("Verwende Posts-Verzeichnis: {:?}", path), None, None);
return path; return path;
} }
} }
@@ -228,9 +228,9 @@ fn get_posts_directory() -> PathBuf {
let fallback_path = PathBuf::from("./posts"); let fallback_path = PathBuf::from("./posts");
if !fallback_path.exists() { if !fallback_path.exists() {
if let Err(e) = fs::create_dir_all(&fallback_path) { if let Err(e) = fs::create_dir_all(&fallback_path) {
add_log("error", &format!("Failed to create posts directory: {}", e), None, None); add_log("error", &format!("Fehler beim Erstellen des Posts-Verzeichnisses: {}", e), None, None);
} else { } else {
add_log("info", "Created posts directory: ./posts", None, None); add_log("info", "Posts-Verzeichnis erstellt: ./posts", None, None);
} }
} }
fallback_path fallback_path
@@ -242,13 +242,13 @@ fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
let mut errors = Vec::new(); let mut errors = Vec::new();
if !dir.exists() { if !dir.exists() {
let error_msg = format!("Directory does not exist: {:?}", dir); let error_msg = format!("Verzeichnis existiert nicht: {:?}", dir);
add_log("error", &error_msg, None, None); add_log("error", &error_msg, None, None);
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg)); return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg));
} }
if !dir.is_dir() { if !dir.is_dir() {
let error_msg = format!("Path is not a directory: {:?}", dir); let error_msg = format!("Pfad ist kein Verzeichnis: {:?}", dir);
add_log("error", &error_msg, None, None); add_log("error", &error_msg, None, None);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, error_msg)); return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, error_msg));
} }
@@ -257,7 +257,7 @@ fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
let entries = match fs::read_dir(dir) { let entries = match fs::read_dir(dir) {
Ok(entries) => entries, Ok(entries) => entries,
Err(e) => { Err(e) => {
add_log("error", &format!("Failed to read directory {:?}: {}", dir, e), None, None); add_log("error", &format!("Fehler beim Lesen des Verzeichnisses {:?}: {}", dir, e), None, None);
return Err(e); return Err(e);
} }
}; };
@@ -279,7 +279,7 @@ fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
match find_markdown_files(&path) { match find_markdown_files(&path) {
Ok(subfiles) => files.extend(subfiles), Ok(subfiles) => files.extend(subfiles),
Err(e) => { Err(e) => {
let error_msg = format!("Error scanning subdirectory {:?}: {}", path, e); let error_msg = format!("Fehler beim Scannen des Unterverzeichnisses {:?}: {}", path, e);
add_log("warning", &error_msg, None, None); add_log("warning", &error_msg, None, None);
errors.push(error_msg); errors.push(error_msg);
} }
@@ -293,7 +293,7 @@ fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
} }
} }
Err(e) => { Err(e) => {
let error_msg = format!("Cannot access file {:?}: {}", path, e); let error_msg = format!("Datei nicht zugänglich {:?}: {}", path, e);
add_log("warning", &error_msg, None, None); add_log("warning", &error_msg, None, None);
errors.push(error_msg); errors.push(error_msg);
} }
@@ -301,7 +301,7 @@ fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
} }
} }
Err(e) => { Err(e) => {
let error_msg = format!("Error reading directory entry: {}", e); let error_msg = format!("Fehler beim Lesen des Verzeichniseintrags: {}", e);
add_log("warning", &error_msg, None, None); add_log("warning", &error_msg, None, None);
errors.push(error_msg); errors.push(error_msg);
} }
@@ -309,9 +309,9 @@ fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
} }
// Log summary // Log summary
add_log("info", &format!("Found {} markdown files in {:?}", files.len(), dir), None, None); add_log("info", &format!("{} Markdown-Dateien in {:?} gefunden", files.len(), dir), None, None);
if !errors.is_empty() { if !errors.is_empty() {
add_log("warning", &format!("Encountered {} errors during directory scan", errors.len()), None, None); add_log("warning", &format!("{} Fehler während der Verzeichnissuche aufgetreten", errors.len()), None, None);
} }
Ok(files) Ok(files)
@@ -396,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() {
@@ -415,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();
@@ -490,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();
@@ -512,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);
} }
@@ -524,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());
} }
@@ -545,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();
@@ -580,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());
} }
@@ -634,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();
@@ -649,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();
@@ -668,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)
} }
@@ -715,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;
} }
} }
@@ -760,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();
@@ -778,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)),
} }
} }
@@ -794,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)),
} }
} }
@@ -824,26 +824,26 @@ pub fn get_parser_logs() -> Vec<LogEntry> {
pub fn clear_parser_logs() { pub fn clear_parser_logs() {
PARSER_LOGS.write().unwrap().clear(); PARSER_LOGS.write().unwrap().clear();
if let Err(e) = save_parser_logs_to_disk_inner(&VecDeque::new()) { if let Err(e) = save_parser_logs_to_disk_inner(&VecDeque::new()) {
eprintln!("Failed to save empty logs to disk: {}", e); eprintln!("Fehler beim Speichern leerer Protokolle auf Festplatte: {}", e);
} }
} }
// Force reinterpret all posts by clearing cache and re-parsing // Force reinterpret all posts by clearing cache and re-parsing
pub fn force_reinterpret_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> { pub fn force_reinterpret_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> {
add_log("info", "Starting force reinterpret of all posts", None, None); add_log("info", "Starte erzwungene Neuinterpretation aller Posts", None, None);
// Clear all caches // Clear all caches
POST_CACHE.write().unwrap().clear(); POST_CACHE.write().unwrap().clear();
ALL_POSTS_CACHE.write().unwrap().take(); ALL_POSTS_CACHE.write().unwrap().take();
POST_STATS.write().unwrap().clear(); POST_STATS.write().unwrap().clear();
add_log("info", "Cleared all caches", None, None); add_log("info", "Alle Caches geleert", None, None);
// Get posts directory and find all markdown files // Get posts directory and find all markdown files
let posts_dir = get_posts_directory(); let posts_dir = get_posts_directory();
let markdown_files = find_markdown_files(&posts_dir)?; let markdown_files = find_markdown_files(&posts_dir)?;
add_log("info", &format!("Found {} markdown files to reinterpret", markdown_files.len()), None, None); add_log("info", &format!("{} Markdown-Dateien zur Neuinterpretation gefunden", markdown_files.len()), None, None);
let mut posts = Vec::new(); let mut posts = Vec::new();
let mut success_count = 0; let mut success_count = 0;
@@ -855,11 +855,11 @@ pub fn force_reinterpret_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Er
Ok(post) => { Ok(post) => {
posts.push(post); posts.push(post);
success_count += 1; success_count += 1;
add_log("info", &format!("Successfully reinterpreted: {}", slug), Some(&slug), None); add_log("info", &format!("Erfolgreich neuinterpretiert: {}", slug), Some(&slug), None);
} }
Err(e) => { Err(e) => {
error_count += 1; error_count += 1;
add_log("error", &format!("Failed to reinterpret {}: {}", slug, e), Some(&slug), None); add_log("error", &format!("Fehler bei der Neuinterpretation von {}: {}", slug, e), Some(&slug), None);
} }
} }
} }
@@ -870,14 +870,14 @@ pub fn force_reinterpret_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Er
// Save cache to disk // Save cache to disk
save_post_cache_to_disk(); save_post_cache_to_disk();
add_log("info", &format!("Force reinterpret completed. Success: {}, Errors: {}", success_count, error_count), None, None); add_log("info", &format!("Erzwungene Neuinterpretation abgeschlossen. Erfolgreich: {}, Fehler: {}", success_count, error_count), None, None);
Ok(posts) Ok(posts)
} }
// Force reparse a single post by clearing its cache and re-parsing // 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>> { pub fn force_reparse_single_post(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
add_log("info", &format!("Starting force reparse of post: {}", slug), Some(slug), None); add_log("info", &format!("Starte erzwungenes Neuparsing des Posts: {}", slug), Some(slug), None);
// Clear this specific post from all caches // Clear this specific post from all caches
POST_CACHE.write().unwrap().remove(slug); POST_CACHE.write().unwrap().remove(slug);
@@ -886,7 +886,7 @@ pub fn force_reparse_single_post(slug: &str) -> Result<Post, Box<dyn std::error:
// Clear the all posts cache since it might contain this post // Clear the all posts cache since it might contain this post
ALL_POSTS_CACHE.write().unwrap().take(); ALL_POSTS_CACHE.write().unwrap().take();
add_log("info", &format!("Cleared cache for post: {}", slug), Some(slug), None); add_log("info", &format!("Cache für Post geleert: {}", slug), Some(slug), None);
// Re-parse the post // Re-parse the post
let post = get_post_by_slug(slug)?; let post = get_post_by_slug(slug)?;
@@ -905,7 +905,7 @@ pub fn force_reparse_single_post(slug: &str) -> Result<Post, Box<dyn std::error:
// Save cache to disk // Save cache to disk
save_post_cache_to_disk(); save_post_cache_to_disk();
add_log("info", &format!("Successfully reparsed post: {}", slug), Some(slug), None); add_log("info", &format!("Post erfolgreich neugeparst: {}", slug), Some(slug), None);
Ok(post) 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 [

View File

@@ -6,35 +6,4 @@ author: rattatwinko
summary: This is the about page summary: This is the about page
--- ---
# About Me _**config this in the monaco editor in the admin panel**_
_**I am rattatwinko**_
I created this Project because of the lack of Blog's that use Markdown.
It really is sad that there are so many blog platforms which are shit.
## What I used:
- TypeScript
- Next.JS
- Rust
- Monaco (for a beautiful Editing experience)
- More shit which you can check out in the Repo
## What I do
School.
Coding.
Not more not less.
### Socials
<!-- HTML for this cause Markdown does not support this -->
<form action="https://instagram.com/rattatwinko">
<input type="submit" value="Insta" />
</form>
<form action="https://tiktok.com/rattatwinko">
<input type="submit" value="TikTok" />
</form>

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

@@ -4,6 +4,7 @@ import dynamic from "next/dynamic";
import { useRouter } from "next/navigation"; 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 });
@@ -178,7 +179,7 @@ export default function EditorPage() {
// 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);
}, []); }, []);
@@ -187,7 +188,7 @@ 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 => {
const { frontmatter, content } = extractFrontmatter(data.raw || data.content || ""); const { frontmatter, content } = extractFrontmatter(data.raw || data.content || "");
@@ -206,7 +207,7 @@ export default function EditorPage() {
try { try {
// First save the file // First save the file
const saveResponse = await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, { const saveResponse = await fetch(withBaseUrl(`/api/posts/${encodeURIComponent(selectedSlug)}`), {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ markdown: fileContent }) body: JSON.stringify({ markdown: fileContent })
@@ -217,7 +218,7 @@ export default function EditorPage() {
} }
// Then call Rust backend to reparse this specific post // Then call Rust backend to reparse this specific post
const reparseResponse = await fetch(`/api/admin/posts?reparsePost=${encodeURIComponent(selectedSlug)}`); const reparseResponse = await fetch(withBaseUrl(`/api/admin/posts?reparsePost=${encodeURIComponent(selectedSlug)}`));
if (!reparseResponse.ok) { if (!reparseResponse.ok) {
console.warn('Failed to reparse post, but file was saved'); console.warn('Failed to reparse post, but file was saved');

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) {
@@ -105,14 +106,14 @@ export default function RustStatusPage() {
const reinterpretAllPosts = async () => { const reinterpretAllPosts = async () => {
try { try {
const res = await fetch('/api/admin/posts?reinterpretAll=1'); const res = await fetch(withBaseUrl('/api/admin/posts?reinterpretAll=1'));
if (!res.ok) throw new Error('Fehler beim Neuinterpretieren der Posts'); if (!res.ok) throw new Error('Fehler beim Neuinterpretieren der Posts');
const data = await res.json(); const data = await res.json();
console.log('Reinterpret result:', data); console.log('Neu-Interpretier Ergebins:', data);
// Refresh all data after reinterpret // Refresh all data after reinterpret
await Promise.all([fetchStats(), fetchHealth(), fetchLogs()]); await Promise.all([fetchStats(), fetchHealth(), fetchLogs()]);
} catch (e: any) { } catch (e: any) {
console.error('Error reinterpreting posts:', e); console.error('Fehler beim Neu-Interpretieren => ', e);
} }
}; };
@@ -189,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"
> >
@@ -374,16 +375,16 @@ export default function RustStatusPage() {
<button <button
onClick={reinterpretAllPosts} onClick={reinterpretAllPosts}
className="px-2.5 py-1.5 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs transition-colors" className="px-2.5 py-1.5 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs transition-colors"
title="Force reinterpret all posts" title="Neuinterpretation aller Beiträge erzwingen"
> >
Reinterpret All Alle Posts neu Interpretieren?
</button> </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>
@@ -405,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

@@ -23,6 +23,7 @@ import matter from 'gray-matter';
import dynamicImport 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 { withBaseUrl } from '@/lib/baseUrl';
const MonacoEditor = dynamicImport(() => import('./MonacoEditor'), { ssr: false }); const MonacoEditor = dynamicImport(() => import('./MonacoEditor'), { ssr: false });
// Import monaco-vim only on client side // Import monaco-vim only on client side
let initVimMode: any = null; let initVimMode: any = null;
@@ -162,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));
@@ -170,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) {
@@ -189,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' }),
@@ -213,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',
@@ -251,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',
@@ -347,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,
}); });
@@ -381,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',
@@ -414,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 }),
@@ -453,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' }),
@@ -464,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 }),
@@ -484,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;
@@ -516,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({
@@ -606,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'));
} }
}; };
@@ -693,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}`);
} }
@@ -706,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 || [] }),
@@ -886,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' }}
@@ -902,7 +903,7 @@ 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 Bearbeiter" title="Markdown Bearbeiter"
style={{ minWidth: '160px' }} style={{ minWidth: '160px' }}
@@ -1477,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

@@ -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))