Adam 1 ヶ月 前
コミット
0e2c25fa2e
9 ファイル変更276 行追加99 行削除
  1. 5 16
      Caddyfile
  2. 123 0
      backend/server.js
  3. 1 1
      backend/themes.json
  4. 10 0
      package-lock.json
  5. 1 0
      package.json
  6. 1 0
      public/posts/index.json
  7. 79 67
      src/App.jsx
  8. 32 15
      src/index.css
  9. 24 0
      src/utils/markdownParser.js

+ 5 - 16
Caddyfile

@@ -1,20 +1,9 @@
 goonblog.thevakhovske.eu.org {
-    root * /var/www/gooneral-wheelchair/dist
-    encode gzip zstd
-    file_server
+     encode gzip zstd
 
-    # Handle routes for stuffs
-    rewrite /posts* /index.html
-    rewrite /login* /index.html
-    rewrite /admin* /index.html
+    # All traffic handled by Node.js backend to support SSR/Meta injection
+    reverse_proxy localhost:3001
 
-    # API routes - proxy to backend
-    handle /api/* {
-        reverse_proxy localhost:3001
-    }
-
-    # Health check endpoint
-    handle /health {
-        reverse_proxy localhost:3001
-    }
+    # Keep health check accessible if needed (though backend handles it now)
+    # handle /health { reverse_proxy localhost:3001 } 
 }

+ 123 - 0
backend/server.js

@@ -757,6 +757,129 @@ app.get("/api/health", (req, res) => {
     res.json({ status: "OK", timestamp: new Date().toISOString() });
 });
 
+// SERVE FRONTEND (SSR-lite for Meta Tags)
+const DIST_DIR = path.join(__dirname, "../dist");
+const INDEX_HTML = path.join(DIST_DIR, "index.html");
+
+// Serve static assets with aggressive caching
+// Vite assets are hashed (e.g., index.h4124j.js), so they are immutable.
+app.use(
+    "/assets",
+    express.static(path.join(DIST_DIR, "assets"), {
+        maxAge: "1y", // 1 year
+        immutable: true,
+        setHeaders: (res, path) => {
+            res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
+        },
+    }),
+);
+
+// Serve other static files (favicon, robots, etc.) with default caching
+app.use(express.static(DIST_DIR, { index: false }));
+
+// Helper to inject meta tags
+const injectMetaTags = async (html, metadata, url) => {
+    let injected = html;
+
+    // Default values
+    const title = metadata.title || "Gooneral Wheelchair";
+    const description = metadata.description || "A blog about stuff.";
+    const image = metadata.image || "https://goonblog.thevakhovske.eu.org/og-image.jpg"; // Fallback image
+
+    // Replace Title
+    injected = injected.replace(/<title>.*<\/title>/, `<title>${title}</title>`);
+
+    // Meta Tags to Inject
+    const metaTags = `
+    <meta property="og:title" content="${title}" />
+    <meta property="og:description" content="${description}" />
+    <meta property="og:image" content="${image}" />
+    <meta property="og:url" content="${url}" />
+    <meta property="og:type" content="article" />
+    <meta name="twitter:card" content="summary_large_image" />
+    <meta name="twitter:title" content="${title}" />
+    <meta name="twitter:description" content="${description}" />
+    <meta name="twitter:image" content="${image}" />
+    `;
+
+    // Inject before </head>
+    return injected.replace("</head>", `${metaTags}</head>`);
+};
+
+
+// Handle Post Routes for SSR
+app.get("/posts/:slug", async (req, res) => {
+    try {
+        const { slug } = req.params;
+        const filename = `${slug}.md`;
+        const filePath = path.join(POSTS_DIR, filename);
+
+        // Read index.html template
+        let html = await fs.readFile(INDEX_HTML, "utf8");
+
+        if (await fs.pathExists(filePath)) {
+            const content = await fs.readFile(filePath, "utf8");
+            const metadata = parsePostMetadata(content);
+
+            // Try to find the first image in the content for the OG image
+            const imageMatch = content.match(/!\[.*?\]\((.*?)\)/);
+            let imageUrl = null;
+            if (imageMatch) {
+                imageUrl = imageMatch[1];
+                // Ensure absolute URL
+                if (imageUrl.startsWith("/")) {
+                    imageUrl = `${req.protocol}://${req.get("host")}${imageUrl}`;
+                }
+            }
+
+            // Check access (Hidden posts)
+            // If hidden and not admin, serve standard index.html (SPA will handle 404/auth check client-side)
+            // OR we can pretend it doesn't exist to bots.
+            const isAdmin = req.session && req.session.user && req.session.user.role === "admin";
+            if (metadata.hidden && !isAdmin) {
+                // Determine behavior: 
+                // If we send raw index.html, client app loads and shows "Not Found" or "Login".
+                // We'll just send raw index.html without generic meta tags? Or default tags?
+                // Let's send default tags to avoid leaking info.
+                res.send(html);
+                return;
+            }
+
+            const pageMetadata = {
+                title: metadata.title,
+                description: metadata.description,
+                image: imageUrl
+            };
+
+            const finalHtml = await injectMetaTags(html, pageMetadata, `${req.protocol}://${req.get("host")}${req.originalUrl}`);
+            res.send(finalHtml);
+        } else {
+            // Post not found - serve SPA to handle 404
+            res.send(html);
+        }
+    } catch (err) {
+        console.error("SSR Error:", err);
+        // Fallback to serving static file if something fails
+        res.sendFile(INDEX_HTML);
+    }
+});
+
+// Catch-all route for SPA
+app.get("*", async (req, res) => {
+    // For other routes (e.g. /, /admin, /login), serve index.html
+    // We can also inject default meta tags here if we want.
+    try {
+        if (await fs.pathExists(INDEX_HTML)) {
+            res.sendFile(INDEX_HTML);
+        } else {
+            res.status(404).send("Frontend build not found");
+        }
+
+    } catch (err) {
+        res.status(500).send("Server Error");
+    }
+});
+
 // Generate initial index on startup
 await generateIndex();
 

+ 1 - 1
backend/themes.json

@@ -1,5 +1,5 @@
 {
-  "activeTheme": "forest",
+  "activeTheme": "default",
   "customThemes": [],
   "builtInThemes": [
     {

+ 10 - 0
package-lock.json

@@ -12,6 +12,7 @@
         "@uiw/react-markdown-preview": "^5.1.5",
         "@uiw/react-md-editor": "^4.0.8",
         "dompurify": "^3.2.6",
+        "highlight.js": "^11.11.1",
         "markdown-it": "^14.1.0",
         "markdown-it-emoji": "^3.0.0",
         "markdown-it-footnote": "^4.0.0",
@@ -3854,6 +3855,15 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/highlight.js": {
+      "version": "11.11.1",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+      "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/html-url-attributes": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "@uiw/react-markdown-preview": "^5.1.5",
     "@uiw/react-md-editor": "^4.0.8",
     "dompurify": "^3.2.6",
+    "highlight.js": "^11.11.1",
     "markdown-it": "^14.1.0",
     "markdown-it-emoji": "^3.0.0",
     "markdown-it-footnote": "^4.0.0",

+ 1 - 0
public/posts/index.json

@@ -1,5 +1,6 @@
 [
   "20251213-hey-there.md",
   "20251217-feature-showcase-advanced-mark.md",
+  "20251217-syntax-test.md",
   "20251217-test.md"
 ]

+ 79 - 67
src/App.jsx

@@ -9,19 +9,29 @@ import {
 import DOMPurify from "dompurify";
 import { AuthProvider, useAuth } from "./contexts/AuthContext";
 import { ThemeProvider } from "./contexts/ThemeContext";
-import AdminDashboard from "./components/AdminDashboard";
-import PostEditor from "./components/PostEditor";
-import LoginForm from "./components/LoginForm";
+// Lazy load components for performance
+const AdminDashboard = React.lazy(() => import("./components/AdminDashboard"));
+const PostEditor = React.lazy(() => import("./components/PostEditor"));
+const LoginForm = React.lazy(() => import("./components/LoginForm"));
+const ThemesManager = React.lazy(() => import("./components/ThemesManager"));
+const ThemeEditor = React.lazy(() => import("./components/ThemeEditor"));
+const MediaManager = React.lazy(() => import("./components/MediaManager"));
+
 import ProtectedRoute from "./components/ProtectedRoute";
-import ThemesManager from "./components/ThemesManager";
-import ThemeEditor from "./components/ThemeEditor";
-import MediaManager from "./components/MediaManager";
 import { createMarkdownParser } from "./utils/markdownParser";
 import { API_BASE } from "./config";
 
 // Initialize the shared markdown parser
 const md = createMarkdownParser();
 
+// Loading Fallback Component
+const LoadingSpinner = () => (
+    <div className="flex justify-center items-center p-12">
+        <div className="animate-spin rounded-full h-8 w-8 border-b-2 theme-primary"></div>
+    </div>
+);
+
+
 // Lightbox Component
 const Lightbox = ({ src, alt, onClose }) => {
     if (!src) return null;
@@ -572,67 +582,69 @@ function App() {
                             onClose={closeLightbox}
                         />
                     )}
-                    <Routes>
-                        <Route path="/" element={<BlogHome />} />
-                        <Route path="/posts/:slug" element={<PostView onImageClick={openLightbox} />} />
-                        <Route path="/login" element={<LoginForm />} />
-                        <Route
-                            path="/admin"
-                            element={
-                                <ProtectedRoute>
-                                    <AdminDashboard />
-                                </ProtectedRoute>
-                            }
-                        />
-                        <Route
-                            path="/admin/post/new"
-                            element={
-                                <ProtectedRoute>
-                                    <PostEditor />
-                                </ProtectedRoute>
-                            }
-                        />
-                        <Route
-                            path="/admin/post/:slug/edit"
-                            element={
-                                <ProtectedRoute>
-                                    <PostEditor />
-                                </ProtectedRoute>
-                            }
-                        />
-                        <Route
-                            path="/admin/themes"
-                            element={
-                                <ProtectedRoute>
-                                    <ThemesManager />
-                                </ProtectedRoute>
-                            }
-                        />
-                        <Route
-                            path="/admin/themes/new"
-                            element={
-                                <ProtectedRoute>
-                                    <ThemeEditor />
-                                </ProtectedRoute>
-                            }
-                        />
-                        <Route
-                            path="/admin/themes/:themeId/edit"
-                            element={
-                                <ProtectedRoute>
-                                    <ThemeEditor />
-                                </ProtectedRoute>
-                            }
-                        />
-                        <Route
-                            path="/admin/media"
-                            element={
-                                <ProtectedRoute>
-                                    <MediaManager />
-                                </ProtectedRoute>
-                            }
-                        />
-                    </Routes>
+                    <React.Suspense fallback={<LoadingSpinner />}>
+                        <Routes>
+                            <Route path="/" element={<BlogHome />} />
+                            <Route path="/posts/:slug" element={<PostView onImageClick={openLightbox} />} />
+                            <Route path="/login" element={<LoginForm />} />
+                            <Route
+                                path="/admin"
+                                element={
+                                    <ProtectedRoute>
+                                        <AdminDashboard />
+                                    </ProtectedRoute>
+                                }
+                            />
+                            <Route
+                                path="/admin/post/new"
+                                element={
+                                    <ProtectedRoute>
+                                        <PostEditor />
+                                    </ProtectedRoute>
+                                }
+                            />
+                            <Route
+                                path="/admin/post/:slug/edit"
+                                element={
+                                    <ProtectedRoute>
+                                        <PostEditor />
+                                    </ProtectedRoute>
+                                }
+                            />
+                            <Route
+                                path="/admin/themes"
+                                element={
+                                    <ProtectedRoute>
+                                        <ThemesManager />
+                                    </ProtectedRoute>
+                                }
+                            />
+                            <Route
+                                path="/admin/themes/new"
+                                element={
+                                    <ProtectedRoute>
+                                        <ThemeEditor />
+                                    </ProtectedRoute>
+                                }
+                            />
+                            <Route
+                                path="/admin/themes/:themeId/edit"
+                                element={
+                                    <ProtectedRoute>
+                                        <ThemeEditor />
+                                    </ProtectedRoute>
+                                }
+                            />
+                            <Route
+                                path="/admin/media"
+                                element={
+                                    <ProtectedRoute>
+                                        <MediaManager />
+                                    </ProtectedRoute>
+                                }
+                            />
+                        </Routes>
+                    </React.Suspense>
                 </ThemeProvider>
             </AuthProvider>
         </Router>

+ 32 - 15
src/index.css

@@ -124,30 +124,47 @@ body {
     margin: 2rem 0;
 }
 
-/* Code blocks */
+/* Markdown Content Styles - FORCE ATOM ONE DARK */
 .markdown-content pre {
-    background-color: var(--color-surface, #1f2937);
-    padding: 1rem;
-    border-radius: 0.5rem;
+    display: block;
     overflow-x: auto;
-    margin: 1.5rem 0;
-    border: 1px solid var(--color-border, #374151);
+    padding: 1em;
+    background: #282c34 !important;
+    /* Force Dark Background */
+    color: #abb2bf !important;
+    /* Force Light Text */
+    border-radius: 0.5rem;
+    margin-bottom: 1.5rem;
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+    font-size: 0.9em;
+    line-height: 1.5;
 }
 
-.markdown-content pre code {
-    background: none;
-    color: var(--color-text, #e5e7eb);
-    padding: 0;
-    font-size: 0.9rem;
+/* Ensure .hljs class shares the same styles if specific overrides exist */
+.markdown-content pre.hljs {
+    background: #282c34 !important;
+    color: #abb2bf !important;
 }
 
-/* Inline code */
 .markdown-content code {
-    font-family: "Courier New", "Noto Color Emoji", Courier, monospace;
-    background-color: var(--color-surface, #374151);
-    color: var(--color-accent, #a7f3d0);
+    /* Inline code styles - adapt to theme */
+    background-color: rgba(var(--text-color), 0.1);
     padding: 0.2em 0.4em;
     border-radius: 0.25rem;
+    font-size: 0.9em;
+}
+
+/* Hard reset for code inside pre to prevent inline styles leaking */
+.markdown-content pre code,
+.markdown-content pre.hljs code {
+    background-color: transparent !important;
+    padding: 0;
+    border-radius: 0;
+    font-size: inherit;
+    color: inherit !important;
+    border: none;
+    box-shadow: none;
 }
 
 /* Emphasis */

+ 24 - 0
src/utils/markdownParser.js

@@ -172,12 +172,36 @@ const imageComparisonPlugin = (md) => {
     };
 };
 
+import hljs from "highlight.js";
+
+// ... existing imports ...
+
 export const createMarkdownParser = () => {
     const md = new MarkdownIt({
         html: true,
         linkify: true,
         typographer: true,
         breaks: false,
+        highlight: function (str, lang) {
+            if (lang && hljs.getLanguage(lang)) {
+                try {
+                    return (
+                        '<pre class="hljs"><code>' +
+                        hljs.highlight(str, {
+                            language: lang,
+                            ignoreIllegals: true,
+                        }).value +
+                        "</code></pre>"
+                    );
+                } catch (__) { }
+            }
+
+            return (
+                '<pre class="hljs"><code>' +
+                md.utils.escapeHtml(str) +
+                "</code></pre>"
+            );
+        },
     })
         .use(scrollableTablesPlugin)
         .use(emoji)