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_LINK_IF_TRUE="http://github.com/ZockerKatze" # If you have GitHub then paste your link here #
|
||||
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
|
||||
- **📊 Enhanced Rust Status**: Real-time parser performance monitoring
|
||||
- **🔍 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
|
||||
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();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../.next/server/pages/index.html'));
|
||||
|
||||
@@ -7,6 +7,10 @@ const nextConfig = {
|
||||
experimental: {
|
||||
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
|
||||
async headers() {
|
||||
return [
|
||||
|
||||
@@ -6,35 +6,4 @@ author: rattatwinko
|
||||
summary: This is the about page
|
||||
---
|
||||
|
||||
# About Me
|
||||
|
||||
_**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>
|
||||
|
||||
_**config this in the monaco editor in the admin panel**_
|
||||
|
||||
@@ -535,3 +535,9 @@ If you have seen this is not very mindfull of browser resources tho.
|
||||
> *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer
|
||||
>
|
||||
> <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';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
const InfoIcon = (
|
||||
<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"
|
||||
color="#2563eb"
|
||||
icon={InfoIcon}
|
||||
onClick={() => router.push('/posts/about')}
|
||||
onClick={() => router.push(withBaseUrl('/posts/about'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
const LockIcon = (
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none">
|
||||
@@ -19,7 +20,7 @@ export default function HeaderButtons() {
|
||||
return (
|
||||
<div className="flex gap-2 justify-center sm:justify-end">
|
||||
<a
|
||||
href="/admin"
|
||||
href={withBaseUrl('/admin')}
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
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
|
||||
href="/posts/about"
|
||||
href={withBaseUrl('/posts/about')}
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
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 Link from 'next/link';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface MobileNavProps {
|
||||
blogOwner: string;
|
||||
@@ -54,29 +55,17 @@ export default function MobileNav({ blogOwner }: MobileNavProps) {
|
||||
<h2 className="text-lg font-bold mb-6">{blogOwner}'s Blog</h2>
|
||||
|
||||
<nav className="space-y-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<a href={withBaseUrl('/')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||
🏠 Home
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
<Link
|
||||
href="/admin"
|
||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<a href={withBaseUrl('/admin')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||
🔐 Admin
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
<Link
|
||||
href="/posts/about"
|
||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<a href={withBaseUrl('/posts/about')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||
👤 About Me
|
||||
</Link>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
@@ -23,7 +24,7 @@ export default function AboutPage() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch("/api/posts/about");
|
||||
const response = await fetch(withBaseUrl('/api/posts/about'));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/navigation";
|
||||
import "@fontsource/jetbrains-mono";
|
||||
import { marked } from "marked";
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||
|
||||
@@ -178,7 +179,7 @@ export default function EditorPage() {
|
||||
|
||||
// Fetch file tree
|
||||
useEffect(() => {
|
||||
fetch("/api/posts")
|
||||
fetch(withBaseUrl('/api/posts'))
|
||||
.then(r => r.json())
|
||||
.then(setTree);
|
||||
}, []);
|
||||
@@ -187,7 +188,7 @@ export default function EditorPage() {
|
||||
useEffect(() => {
|
||||
if (!selectedSlug) return;
|
||||
setLoading(true);
|
||||
fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`)
|
||||
fetch(withBaseUrl(`/api/posts/${encodeURIComponent(selectedSlug)}`))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const { frontmatter, content } = extractFrontmatter(data.raw || data.content || "");
|
||||
@@ -206,7 +207,7 @@ export default function EditorPage() {
|
||||
|
||||
try {
|
||||
// First save the file
|
||||
const saveResponse = await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, {
|
||||
const saveResponse = await fetch(withBaseUrl(`/api/posts/${encodeURIComponent(selectedSlug)}`), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ markdown: fileContent })
|
||||
@@ -217,7 +218,7 @@ export default function EditorPage() {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
console.warn('Failed to reparse post, but file was saved');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
type: 'post';
|
||||
@@ -26,7 +27,7 @@ type Node = Post | Folder;
|
||||
// Helper to get folder details
|
||||
async function getFolderDetails(path: string): Promise<{ created: string, items: number, size: number, error?: string }> {
|
||||
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');
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
@@ -38,7 +39,7 @@ async function getFolderDetails(path: string): Promise<{ created: string, items:
|
||||
// Helper to get post size and creation date
|
||||
async function getPostSize(slug: string): Promise<{ size: number | null, created: string | null }> {
|
||||
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');
|
||||
const data = await res.json();
|
||||
return { size: data.size, created: data.created };
|
||||
@@ -76,7 +77,7 @@ export default function ManagePage() {
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/posts');
|
||||
const response = await fetch(withBaseUrl('/api/posts'));
|
||||
const data = await response.json();
|
||||
setNodes(data);
|
||||
} catch (error) {
|
||||
@@ -140,7 +141,7 @@ export default function ManagePage() {
|
||||
type: deleteConfirm.item.type
|
||||
});
|
||||
|
||||
const response = await fetch('/api/admin/delete', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/delete'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -170,7 +171,7 @@ export default function ManagePage() {
|
||||
// Move post API call
|
||||
const movePost = async (post: Post, targetFolder: string[]) => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/posts/move', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/posts/move'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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:items-center gap-3 sm:gap-4">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Inhaltsverwaltung</h1>
|
||||
<Link
|
||||
href="/admin"
|
||||
<a
|
||||
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"
|
||||
>
|
||||
Zum Admin-Panel
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -461,7 +462,7 @@ export default function ManagePage() {
|
||||
onClick={async () => {
|
||||
if (!deleteAllConfirm.folder) return;
|
||||
// Call delete API with recursive flag
|
||||
await fetch('/api/admin/delete', {
|
||||
await fetch(withBaseUrl('/api/admin/delete'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface PostStats {
|
||||
slug: string;
|
||||
@@ -52,7 +53,7 @@ export default function RustStatusPage() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
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');
|
||||
const data = await res.json();
|
||||
setStats(data);
|
||||
@@ -67,7 +68,7 @@ export default function RustStatusPage() {
|
||||
setHealthLoading(true);
|
||||
setHealthError(null);
|
||||
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');
|
||||
const data = await res.json();
|
||||
setHealth(data);
|
||||
@@ -82,7 +83,7 @@ export default function RustStatusPage() {
|
||||
setLogsLoading(true);
|
||||
setLogsError(null);
|
||||
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');
|
||||
const data = await res.json();
|
||||
setLogs(data);
|
||||
@@ -95,7 +96,7 @@ export default function RustStatusPage() {
|
||||
|
||||
const clearLogs = async () => {
|
||||
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');
|
||||
await fetchLogs(); // Refresh logs after clearing
|
||||
} catch (e: any) {
|
||||
@@ -105,7 +106,7 @@ export default function RustStatusPage() {
|
||||
|
||||
const reinterpretAllPosts = async () => {
|
||||
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');
|
||||
const data = await res.json();
|
||||
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">
|
||||
{/* Back to Admin button */}
|
||||
<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"
|
||||
title="Zurück zur Admin-Panel"
|
||||
>
|
||||
|
||||
@@ -23,6 +23,7 @@ import matter from 'gray-matter';
|
||||
import dynamicImport from 'next/dynamic';
|
||||
import { Theme } from 'emoji-picker-react';
|
||||
import '../highlight-github.css';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
const MonacoEditor = dynamicImport(() => import('./MonacoEditor'), { ssr: false });
|
||||
// Import monaco-vim only on client side
|
||||
let initVimMode: any = null;
|
||||
@@ -162,7 +163,7 @@ export default function AdminPage() {
|
||||
|
||||
useEffect(() => {
|
||||
// Check if docker is used
|
||||
fetch('/api/admin/docker')
|
||||
fetch(withBaseUrl('/api/admin/docker'))
|
||||
.then(res => res.json())
|
||||
.then(data => setIsDocker(!!data.docker))
|
||||
.catch(() => setIsDocker(false));
|
||||
@@ -170,7 +171,7 @@ export default function AdminPage() {
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/posts');
|
||||
const response = await fetch(withBaseUrl('/api/posts'));
|
||||
const data = await response.json();
|
||||
setNodes(data);
|
||||
} catch (error) {
|
||||
@@ -189,7 +190,7 @@ export default function AdminPage() {
|
||||
return;
|
||||
}
|
||||
// Check password via API
|
||||
const res = await fetch('/api/admin/password', {
|
||||
const res = await fetch(withBaseUrl('/api/admin/password'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: pass, mode: 'login' }),
|
||||
@@ -213,7 +214,7 @@ export default function AdminPage() {
|
||||
const handleCreatePost = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/admin/posts', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/posts'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -251,7 +252,7 @@ export default function AdminPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/folders', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/folders'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -347,7 +348,7 @@ export default function AdminPage() {
|
||||
formData.append('file', file);
|
||||
formData.append('path', currentPath.join('/'));
|
||||
|
||||
const response = await fetch('/api/admin/upload', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/upload'), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
@@ -381,7 +382,7 @@ export default function AdminPage() {
|
||||
type: deleteConfirm.item.type
|
||||
});
|
||||
|
||||
const response = await fetch('/api/admin/delete', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/delete'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -414,7 +415,7 @@ export default function AdminPage() {
|
||||
? prev.filter((s) => s !== slug)
|
||||
: [slug, ...prev];
|
||||
// Update pinned.json on the server
|
||||
fetch('/api/admin/posts', {
|
||||
fetch(withBaseUrl('/api/admin/posts'), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pinned: newPinned }),
|
||||
@@ -453,7 +454,7 @@ export default function AdminPage() {
|
||||
return;
|
||||
}
|
||||
// Check old password
|
||||
const res = await fetch('/api/admin/password', {
|
||||
const res = await fetch(withBaseUrl('/api/admin/password'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: changePwOld, mode: 'login' }),
|
||||
@@ -464,7 +465,7 @@ export default function AdminPage() {
|
||||
return;
|
||||
}
|
||||
// Set new password
|
||||
const res2 = await fetch('/api/admin/password', {
|
||||
const res2 = await fetch(withBaseUrl('/api/admin/password'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: changePwNew }),
|
||||
@@ -484,7 +485,7 @@ export default function AdminPage() {
|
||||
// Function to load a post's raw markdown
|
||||
const loadPostRaw = async (slug: string, folderPath: string) => {
|
||||
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) {
|
||||
alert('Error loading post');
|
||||
return;
|
||||
@@ -516,7 +517,7 @@ export default function AdminPage() {
|
||||
summary: newPost.summary,
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -606,9 +607,9 @@ export default function AdminPage() {
|
||||
}
|
||||
closeModal();
|
||||
if (choice === 'docker') {
|
||||
exportFromEndpoint('/api/admin/export');
|
||||
exportFromEndpoint(withBaseUrl('/api/admin/export'));
|
||||
} 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
|
||||
try {
|
||||
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) {
|
||||
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('Saving to pinned.json...');
|
||||
|
||||
const saveRes = await fetch('/api/admin/posts', {
|
||||
const saveRes = await fetch(withBaseUrl('/api/admin/posts'), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folderEmojis, pinned: pinnedData.pinned || [] }),
|
||||
@@ -886,7 +887,7 @@ export default function AdminPage() {
|
||||
</span>
|
||||
</button>
|
||||
<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"
|
||||
title="View Rust Parser Dashboard"
|
||||
style={{ minWidth: '160px' }}
|
||||
@@ -902,7 +903,7 @@ export default function AdminPage() {
|
||||
</a>
|
||||
{/* VS Code Editor Button */}
|
||||
<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"
|
||||
title="Markdown Bearbeiter"
|
||||
style={{ minWidth: '160px' }}
|
||||
@@ -1477,7 +1478,7 @@ export default function AdminPage() {
|
||||
</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>
|
||||
|
||||
@@ -7,6 +7,7 @@ import AboutButton from './AboutButton';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import HeaderButtons from './HeaderButtons';
|
||||
import MobileNav from './MobileNav';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
@@ -46,11 +47,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="de" className="h-full">
|
||||
<Head>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="icon" href={withBaseUrl('/favicon.ico')} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={withBaseUrl('/apple-touch-icon.png')} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={withBaseUrl('/favicon-32x32.png')} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={withBaseUrl('/favicon-16x16.png')} />
|
||||
<link rel="manifest" href={withBaseUrl('/site.webmanifest')} />
|
||||
</Head>
|
||||
<body className={`${inter.className} h-full min-h-screen flex flex-col`}>
|
||||
<MobileNav blogOwner={blogOwner} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
type: 'post';
|
||||
@@ -47,7 +48,7 @@ export default function Home() {
|
||||
|
||||
const setupSSE = () => {
|
||||
try {
|
||||
eventSource = new EventSource('/api/posts/stream');
|
||||
eventSource = new EventSource(withBaseUrl('/api/posts/stream'));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
@@ -101,7 +102,7 @@ export default function Home() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch('/api/posts');
|
||||
const response = await fetch(withBaseUrl('/api/posts'));
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
@@ -55,7 +56,7 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
|
||||
|
||||
const setupSSE = () => {
|
||||
try {
|
||||
eventSource = new EventSource('/api/posts/stream');
|
||||
eventSource = new EventSource(withBaseUrl('/api/posts/stream'));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
@@ -109,7 +110,7 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch(`/api/posts/${encodeURIComponent(slugPath)}`);
|
||||
const response = await fetch(withBaseUrl(`/api/posts/${encodeURIComponent(slugPath)}`));
|
||||
if (!response.ok) {
|
||||
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