diff --git a/package-lock.json b/package-lock.json
index 892ac05..e29fdd1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
"emoji-picker-react": "^4.12.2",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
+ "isomorphic-dompurify": "^2.25.0",
"jsdom": "^24.0.0",
"marked": "^12.0.0",
"monaco-editor": "^0.52.2",
@@ -1212,19 +1213,6 @@
"parse5": "^7.0.0"
}
},
- "node_modules/@types/jsdom/node_modules/parse5": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
- "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "entities": "^6.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -5183,6 +5171,92 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
+ "node_modules/isomorphic-dompurify": {
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz",
+ "integrity": "sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==",
+ "license": "MIT",
+ "dependencies": {
+ "dompurify": "^3.2.6",
+ "jsdom": "^26.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/isomorphic-dompurify/node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/isomorphic-dompurify/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/isomorphic-dompurify/node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/isomorphic-dompurify/node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/iterator.prototype": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
@@ -5327,18 +5401,6 @@
"node": ">= 14"
}
},
- "node_modules/jsdom/node_modules/parse5": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
- "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
- "license": "MIT",
- "dependencies": {
- "entities": "^6.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
"node_modules/jsdom/node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -6314,6 +6376,18 @@
"node": ">=6"
}
},
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8246,6 +8320,24 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "license": "MIT"
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
diff --git a/package.json b/package.json
index b324d03..c234607 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"emoji-picker-react": "^4.12.2",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
+ "isomorphic-dompurify": "^2.25.0",
"jsdom": "^24.0.0",
"marked": "^12.0.0",
"monaco-editor": "^0.52.2",
diff --git a/src/app/admin/MonacoEditor.tsx b/src/app/admin/MonacoEditor.tsx
new file mode 100644
index 0000000..f8c2137
--- /dev/null
+++ b/src/app/admin/MonacoEditor.tsx
@@ -0,0 +1,12 @@
+import Editor from "@monaco-editor/react";
+
+export default function MonacoEditorWrapper(props: any) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 1e0f452..25cd903 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -1,9 +1,15 @@
'use client';
+export const dynamic = "force-dynamic";
/*********************************************
* This is the main admin page for the blog.
*
* Written Jun 19 2025
+* Rewritten fucking 15 times cause of the
+* fucking
+* typescript linter.
+*
+* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault
**********************************************/
import { useState, useEffect, useCallback, useRef } from 'react';
@@ -12,17 +18,21 @@ import Link from 'next/link';
import { marked } from 'marked';
import hljs from 'highlight.js';
import matter from 'gray-matter';
-import dynamic from 'next/dynamic';
+import dynamicImport from 'next/dynamic';
import { Theme } from 'emoji-picker-react';
import '../highlight-github.css';
-import MonacoEditor from '@monaco-editor/react';
-import { initVimMode, VimMode } from 'monaco-vim';
+const MonacoEditor = dynamicImport(() => import('./MonacoEditor'), { ssr: false });
+// Import monaco-vim only on client side
+let initVimMode: any = null;
+let VimMode: any = null;
+
+if (typeof window !== 'undefined') {
+ const monacoVim = require('monaco-vim');
+ initVimMode = monacoVim.initVimMode;
+ VimMode = monacoVim.VimMode;
+}
import '@fontsource/jetbrains-mono';
-// @ts-ignore-next-line
-// eslint-disable-next-line
-// If you want, you can move this to a global.d.ts file
-// declare module 'monaco-vim';
interface Post {
slug: string;
@@ -55,7 +65,7 @@ interface Post {
type Node = Post | Folder;
-const EmojiPicker = dynamic(() => import('emoji-picker-react'), { ssr: false });
+const EmojiPicker = dynamicImport(() => import('emoji-picker-react'), { ssr: false });
// Patch marked renderer to always add 'hljs' class to code blocks
const renderer = new marked.Renderer();
@@ -89,12 +99,7 @@ export default function AdminPage() {
});
const [showManageContent, setShowManageContent] = useState(false);
const [managePath, setManagePath] = useState([]);
- const [pinned, setPinned] = useState(() => {
- if (typeof window !== 'undefined') {
- return JSON.parse(localStorage.getItem('pinnedPosts') || '[]');
- }
- return [];
- });
+ const [pinned, setPinned] = useState([]);
const [pinFeedback, setPinFeedback] = useState(null);
const [showChangePassword, setShowChangePassword] = useState(false);
const [changePwOld, setChangePwOld] = useState('');
@@ -104,12 +109,7 @@ export default function AdminPage() {
const [previewHtml, setPreviewHtml] = useState('');
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
const [isDocker, setIsDocker] = useState(false);
- const [rememberExportChoice, setRememberExportChoice] = useState(() => {
- if (typeof window !== 'undefined') {
- return localStorage.getItem('rememberExportChoice') === 'true';
- }
- return false;
- });
+ const [rememberExportChoice, setRememberExportChoice] = useState(false);
const [lastExportChoice, setLastExportChoice] = useState(null);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(null);
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState(null);
@@ -536,15 +536,7 @@ export default function AdminPage() {
};
function handleExportTarball() {
- // Check if we should use the remembered choice
- if (rememberExportChoice && lastExportChoice) {
- if (lastExportChoice === 'docker') {
- exportFromEndpoint('/api/admin/export');
- } else if (lastExportChoice === 'local') {
- exportFromEndpoint('/api/admin/exportlocal');
- }
- return;
- }
+ if (typeof window === 'undefined') return;
// Create popup modal
const modal = document.createElement('div');
@@ -637,6 +629,7 @@ export default function AdminPage() {
}
function exportFromEndpoint(endpoint: string) {
+ if (typeof window === 'undefined') return;
fetch(endpoint)
.then(async (res) => {
if (!res.ok) throw new Error('Export failed');
@@ -660,6 +653,15 @@ export default function AdminPage() {
setLastExportChoice(null);
};
+ // Hydrate pinned, rememberExportChoice, lastExportChoice from localStorage on client only
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ setPinned(JSON.parse(localStorage.getItem('pinnedPosts') || '[]'));
+ setRememberExportChoice(localStorage.getItem('rememberExportChoice') === 'true');
+ setLastExportChoice(localStorage.getItem('lastExportChoice'));
+ }
+ }, []);
+
// Simple and reliable emoji update handler
const handleSetFolderEmoji = async (folderPath: string, emoji: string) => {
try {
@@ -772,7 +774,7 @@ export default function AdminPage() {
// Attach/detach Vim mode when vimMode changes
useEffect(() => {
- if (vimMode && monacoRef.current) {
+ if (vimMode && monacoRef.current && initVimMode) {
// @ts-ignore
vimInstanceRef.current = initVimMode(monacoRef.current, vimStatusRef.current);
} else if (vimInstanceRef.current) {
@@ -917,7 +919,7 @@ export default function AdminPage() {
-
+
@@ -1182,7 +1184,7 @@ export default function AdminPage() {
height="100%"
defaultLanguage="markdown"
value={newPost.content}
- onChange={(value) => setNewPost({ ...newPost, content: value || '' })}
+ onChange={(value?: string) => setNewPost({ ...newPost, content: value || '' })}
options={{
minimap: { enabled: false },
wordWrap: 'on',
@@ -1193,7 +1195,7 @@ export default function AdminPage() {
automaticLayout: true,
fontFamily: 'JetBrains Mono, monospace',
}}
- onMount={(editor) => {
+ onMount={(editor: any) => {
monacoRef.current = editor;
}}
/>
diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts
index 123c9f8..1df2bc9 100644
--- a/src/app/api/posts/route.ts
+++ b/src/app/api/posts/route.ts
@@ -5,8 +5,7 @@ import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';
-import DOMPurify from 'dompurify';
-import { JSDOM } from 'jsdom';
+import createDOMPurify from 'isomorphic-dompurify';
import hljs from 'highlight.js';
import { getPostsDirectory } from '@/lib/postsDirectory';
@@ -106,10 +105,8 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi
let processedContent = '';
try {
- const rawHtml = marked.parse(content);
- const window = new JSDOM('').window;
- const purify = DOMPurify(window);
- processedContent = purify.sanitize(rawHtml as string, {
+ const rawHtml = marked.parse(content) as string;
+ processedContent = createDOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'a', 'ul', 'ol', 'li', 'blockquote',
@@ -123,7 +120,7 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi
'src', 'alt', 'title', 'width', 'height',
'frameborder', 'allowfullscreen'
],
- ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
+ ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+\.\-]+(?:[^a-z+\.\-:]|$))/i
});
} catch (err) {
console.error(`Error processing markdown for ${relPath}:`, err);