This commit is contained in:
2025-06-08 16:35:06 +09:00
parent 230b3558b9
commit be026db3d8
20 changed files with 182 additions and 73 deletions

View File

@ -1,9 +1,9 @@
import { Routes, Route, Link, useLocation } from "react-router-dom";
import EpisodeList from "./components/EpisodeList";
import FeedManager from "./components/FeedManager";
import FeedList from "./components/FeedList";
import FeedDetail from "./components/FeedDetail";
import { Link, Route, Routes, useLocation } from "react-router-dom";
import EpisodeDetail from "./components/EpisodeDetail";
import EpisodeList from "./components/EpisodeList";
import FeedDetail from "./components/FeedDetail";
import FeedList from "./components/FeedList";
import FeedManager from "./components/FeedManager";
function App() {
const location = useLocation();

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { Link, useParams } from "react-router-dom";
interface EpisodeWithFeedInfo {
id: string;
@ -162,6 +163,66 @@ function EpisodeDetail() {
return (
<div className="container">
<Helmet>
<title>{episode.title} - Voice RSS Summary</title>
<meta
name="description"
content={episode.description || `${episode.title}のエピソード詳細`}
/>
{/* OpenGraph metadata */}
<meta property="og:title" content={episode.title} />
<meta
property="og:description"
content={episode.description || `${episode.title}のエピソード詳細`}
/>
<meta property="og:type" content="article" />
<meta property="og:url" content={window.location.href} />
<meta property="og:site_name" content="Voice RSS Summary" />
<meta property="og:locale" content="ja_JP" />
{/* Image metadata */}
<meta property="og:image" content={`${window.location.origin}/default-thumbnail.svg`} />
<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} - Voice RSS Summary`} />
{/* Twitter Card metadata */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={episode.title} />
<meta
name="twitter:description"
content={episode.description || `${episode.title}のエピソード詳細`}
/>
<meta name="twitter:image" content={`${window.location.origin}/default-thumbnail.svg`} />
<meta name="twitter:image:alt" content={`${episode.title} - Voice RSS Summary`} />
{/* Audio-specific metadata */}
<meta
property="og:audio"
content={
episode.audioPath.startsWith("/")
? `${window.location.origin}${episode.audioPath}`
: `${window.location.origin}/podcast_audio/${episode.audioPath}`
}
/>
<meta property="og:audio:type" content="audio/mpeg" />
{/* 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} />
)}
</Helmet>
<div className="header">
<Link
to="/"
@ -189,7 +250,11 @@ function EpisodeDetail() {
<audio
controls
className="audio-player"
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
src={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
style={{ width: "100%", height: "60px" }}
>
使
@ -277,7 +342,11 @@ function EpisodeDetail() {
<div>
<strong>URL:</strong>
<a
href={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
href={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
target="_blank"
rel="noopener noreferrer"
style={{ marginLeft: "5px" }}

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
interface Episode {
@ -303,7 +303,13 @@ function EpisodeList() {
<div style={{ display: "flex", gap: "8px" }}>
<button
className="btn btn-primary"
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)}
onClick={() =>
playAudio(
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`,
)
}
>
</button>
@ -314,13 +320,20 @@ function EpisodeList() {
</button>
</div>
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
{currentAudio ===
(episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`) && (
<div>
<audio
id={episode.audioPath}
controls
className="audio-player"
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
src={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
onEnded={() => setCurrentAudio(null)}
/>
</div>

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
interface Feed {
id: string;
@ -284,7 +284,13 @@ function FeedDetail() {
<div style={{ display: "flex", gap: "8px" }}>
<button
className="btn btn-primary"
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)}
onClick={() =>
playAudio(
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`,
)
}
>
</button>
@ -295,13 +301,20 @@ function FeedDetail() {
</button>
</div>
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
{currentAudio ===
(episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`) && (
<div>
<audio
id={episode.audioPath}
controls
className="audio-player"
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
src={
episode.audioPath.startsWith("/")
? episode.audioPath
: `/podcast_audio/${episode.audioPath}`
}
onEnded={() => setCurrentAudio(null)}
/>
</div>

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
interface Feed {

View File

@ -1,13 +1,16 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter } from "react-router-dom";
import App from "./App.tsx";
import "./styles.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
<HelmetProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</HelmetProvider>
</StrictMode>,
);