Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c74bb21f |
@@ -11,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!
|
||||||
24
README.md
24
README.md
@@ -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;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -21,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'));
|
||||||
|
|||||||
@@ -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 [
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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'))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}'s Blog</h2>
|
<h2 className="text-lg font-bold mb-6">{blogOwner}'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">
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,7 +106,7 @@ 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('Neu-Interpretier Ergebins:', data);
|
console.log('Neu-Interpretier Ergebins:', data);
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
16
src/lib/baseUrl.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user