Update for static file serving

This commit is contained in:
2025-06-08 16:45:55 +09:00
parent be026db3d8
commit bedc977a00

156
server.ts
View File

@ -119,6 +119,27 @@ app.get("/podcast.xml", async (c) => {
} }
}); });
app.get("/default-thumbnail.svg", async (c) => {
try {
const filePath = path.join(config.paths.publicDir, "default-thumbnail.svg");
const file = Bun.file(filePath);
if (await file.exists()) {
const blob = await file.arrayBuffer();
return c.body(blob, 200, {
"Content-Type": "image/svg+xml",
"Cache-Control": "public, max-age=86400", // Cache for 24 hours
});
}
console.warn("default-thumbnail.svg not found");
return c.notFound();
} catch (error) {
console.error("Error serving default-thumbnail.svg:", error);
return c.notFound();
}
});
// Frontend fallback routes // Frontend fallback routes
async function serveIndex(c: any) { async function serveIndex(c: any) {
try { try {
@ -138,10 +159,139 @@ async function serveIndex(c: any) {
} }
} }
// Function to generate episode page with OG metadata
async function serveEpisodePage(c: any, episodeId: string) {
try {
// First, try to get episode from database
const { fetchEpisodeWithSourceInfo } = await import("./services/database.js");
let episode = null;
try {
episode = await fetchEpisodeWithSourceInfo(episodeId);
} catch (error) {
console.log("Episode not found in database, trying XML fallback");
}
// If not found in database, try XML
if (!episode) {
try {
const xml2js = await import("xml2js");
const fs = await import("fs");
const podcastXmlPath = path.join(config.paths.publicDir, "podcast.xml");
if (fs.existsSync(podcastXmlPath)) {
const xmlContent = fs.readFileSync(podcastXmlPath, "utf-8");
const parser = new xml2js.Parser();
const result = await parser.parseStringPromise(xmlContent);
const items = result?.rss?.channel?.[0]?.item || [];
const targetItem = items.find(
(item: any) => generateEpisodeId(item) === episodeId,
);
if (targetItem) {
episode = {
id: episodeId,
title: targetItem.title?.[0] || "Untitled",
description: targetItem.description?.[0] || "",
createdAt: targetItem.pubDate?.[0] || "",
audioPath: targetItem.enclosure?.[0]?.$?.url || "",
articleLink: targetItem.link?.[0] || "",
articleTitle: targetItem.title?.[0] || "Untitled",
articlePubDate: targetItem.pubDate?.[0] || "",
feedTitle: "RSS Feed",
feedUrl: "",
};
}
}
} catch (error) {
console.error("Error parsing XML for episode:", error);
}
}
// If still no episode found, serve regular index
if (!episode) {
return serveIndex(c);
}
// Generate the HTML with OG metadata
const indexPath = path.join(config.paths.frontendBuildDir, "index.html");
const file = Bun.file(indexPath);
if (!(await file.exists())) {
console.error(`index.html not found at ${indexPath}`);
return c.text("Frontend not built. Run 'bun run build:frontend'", 404);
}
let html = await file.text();
// Extract the base URL
const protocol = c.req.header("x-forwarded-proto") || "http";
const host = c.req.header("host") || "localhost:3000";
const baseUrl = `${protocol}://${host}`;
// Replace title and add OG metadata
const title = `${episode.title} - Voice RSS Summary`;
const description = episode.description || `${episode.title}のエピソード詳細`;
const episodeUrl = `${baseUrl}/episode/${episodeId}`;
const imageUrl = `${baseUrl}/default-thumbnail.svg`;
const audioUrl = episode.audioPath.startsWith('/')
? `${baseUrl}${episode.audioPath}`
: `${baseUrl}/podcast_audio/${episode.audioPath}`;
// Replace the title
html = html.replace(
/<title>.*?<\/title>/,
`<title>${title}</title>`
);
// Add OG metadata before closing </head>
const ogMetadata = `
<meta name="description" content="${description.replace(/"/g, '&quot;')}" />
<!-- OpenGraph metadata -->
<meta property="og:title" content="${episode.title.replace(/"/g, '&quot;')}" />
<meta property="og:description" content="${description.replace(/"/g, '&quot;')}" />
<meta property="og:type" content="article" />
<meta property="og:url" content="${episodeUrl}" />
<meta property="og:site_name" content="Voice RSS Summary" />
<meta property="og:locale" content="ja_JP" />
<meta property="og:image" content="${imageUrl}" />
<meta property="og:image:type" content="image/svg+xml" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="${episode.title.replace(/"/g, '&quot;')} - Voice RSS Summary" />
<meta property="og:audio" content="${audioUrl}" />
<meta property="og:audio:type" content="audio/mpeg" />
<!-- Twitter Card metadata -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${episode.title.replace(/"/g, '&quot;')}" />
<meta name="twitter:description" content="${description.replace(/"/g, '&quot;')}" />
<meta name="twitter:image" content="${imageUrl}" />
<meta name="twitter:image:alt" content="${episode.title.replace(/"/g, '&quot;')} - Voice RSS Summary" />
<!-- Article metadata -->
<meta property="article:published_time" content="${episode.createdAt}" />
${episode.articlePubDate && episode.articlePubDate !== episode.createdAt ?
`<meta property="article:modified_time" content="${episode.articlePubDate}" />` : ''}
${episode.feedTitle ? `<meta property="article:section" content="${episode.feedTitle.replace(/"/g, '&quot;')}" />` : ''}
`;
html = html.replace('</head>', `${ogMetadata}</head>`);
return c.body(html, 200, { "Content-Type": "text/html; charset=utf-8" });
} catch (error) {
console.error("Error serving episode page:", error);
return serveIndex(c);
}
}
app.get("/", serveIndex); app.get("/", serveIndex);
app.get("/index.html", serveIndex); app.get("/index.html", serveIndex);
// API endpoints for frontend // API endpoints for frontend
app.get("/api/episodes", async (c) => { app.get("/api/episodes", async (c) => {
try { try {
@ -376,6 +526,12 @@ app.post("/api/feed-requests", async (c) => {
} }
}); });
// Episode page with OG metadata - must be before catch-all
app.get("/episode/:episodeId", async (c) => {
const episodeId = c.req.param("episodeId");
return serveEpisodePage(c, episodeId);
});
// Catch-all for SPA routing // Catch-all for SPA routing
app.get("*", serveIndex); app.get("*", serveIndex);