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