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