Update for static file serving
This commit is contained in:
156
server.ts
156
server.ts
@ -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
|
||||
async function serveIndex(c: any) {
|
||||
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, '"')}" />
|
||||
|
||||
<!-- OpenGraph metadata -->
|
||||
<meta property="og:title" content="${episode.title.replace(/"/g, '"')}" />
|
||||
<meta property="og:description" content="${description.replace(/"/g, '"')}" />
|
||||
<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, '"')} - 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, '"')}" />
|
||||
<meta name="twitter:description" content="${description.replace(/"/g, '"')}" />
|
||||
<meta name="twitter:image" content="${imageUrl}" />
|
||||
<meta name="twitter:image:alt" content="${episode.title.replace(/"/g, '"')} - 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, '"')}" />` : ''}
|
||||
`;
|
||||
|
||||
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("/index.html", serveIndex);
|
||||
|
||||
|
||||
// API endpoints for frontend
|
||||
app.get("/api/episodes", async (c) => {
|
||||
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
|
||||
app.get("*", serveIndex);
|
||||
|
||||
|
Reference in New Issue
Block a user