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
|
// 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, '"')}" />
|
||||||
|
|
||||||
|
<!-- 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("/", 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);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user