feat(ui): 添加使用说明和常见问题模块并优化SEO配置

- 新增HowToUse和FAQ组件,提供多语言使用指南和常见问题解答
- 优化播客详情页SEO,使用overview_content生成动态标题和描述
- 简化页面元数据配置,更新网站域名和SEO文案
- 更新联系邮箱地址
- 完善中英日三语的多语言配置文件
This commit is contained in:
hex2077
2025-10-21 14:59:57 +08:00
parent dd2a1b536f
commit c2c31227a7
12 changed files with 414 additions and 27 deletions

View File

@@ -18,6 +18,50 @@
"taskCreatedTitle": "Task Created",
"taskCreatedMessage": "Podcast generation task has been started, Task ID",
"recentlyGenerated": "Recently Generated",
"howToUse": "How to Use",
"howToUseSubtitle": "Get started with PodcastHub and create professional podcasts easily",
"faq": "FAQ",
"faqSubtitle": "Answers to common questions about using our service",
"howToUseSteps": {
"step1": {
"title": "1. Input Content",
"description": "Enter the text you want to convert into a podcast in the text box. It can be articles, notes, or any text. Use story generator feature to quickly generate automatic podcast content."
},
"step2": {
"title": "2. Select Mode",
"description": "Choose between standard mode or AI story mode. Our podcast generator helps you create more engaging podcast scripts."
},
"step3": {
"title": "3. Configure Voice",
"description": "Select the appropriate voice role to customize your podcast audio."
},
"step4": {
"title": "4. Generate Podcast",
"description": "Click the generate button, and PodcastHub will automatically create professional podcast audio for you."
}
},
"faqItems": {
"q1": {
"question": "What is PodcastHub?",
"answer": "PodcastHub is an intelligent podcast generation platform that quickly converts your text content into professional podcast audio. It supports multiple voices and AI generation modes."
},
"q2": {
"question": "How to use the story generator feature?",
"answer": "When selecting AI story mode, the system automatically uses the immersive story feature to create vivid AI voice-over scripts."
},
"q3": {
"question": "What languages are supported?",
"answer": "PodcastHub supports multiple languages including Chinese, English, Japanese, and more. You can choose the corresponding voice role based on your content."
},
"q4": {
"question": "How long are generated podcasts saved?",
"answer": "To protect your privacy and save storage space, generated podcast audio is retained for 30 minutes. Please download and save it locally in time."
},
"q5": {
"question": "How to get more credits?",
"answer": "You can purchase credit packages or sign up for free trial credits to get more generation attempts. New users receive free trial credits upon registration."
}
},
"dataRetentionWarning": "Data is only kept for 30 minutes, please download and save it as soon as possible",
"saveSuccessTitle": "Save successfully",
"saveErrorTitle": "Save failed",

View File

@@ -1,5 +1,5 @@
{
"title": "PodcastHub: Your AI Podcast creation platform - easily convert text into high-quality podcast audio, supporting multiple voices and styles, making creativity accessible.",
"description": "PodcastHub uses cutting-edge AI technology to provide unlimited possibilities for your creativity. Easily convert text and ideas into professional-quality podcast audio, with a variety of personalized voice and style options. Experience efficient creation now, spread your voice globally, attract more listeners, and simplify your podcast production process.",
"title": "PodcastHub: AI Podcast Creator - Text to Audio",
"description": "Transform text into professional podcast audio with AI. Multiple voices, styles & languages. Create engaging podcasts effortlessly. Start free today!",
"keywords": "podcast,AI,voice synthesis,TTS,audio generation"
}

View File

@@ -18,6 +18,50 @@
"taskCreatedTitle": "タスク作成済み",
"taskCreatedMessage": "ポッドキャスト生成タスクが開始されました。タスクID",
"recentlyGenerated": "最近生成されたもの",
"howToUse": "使い方",
"howToUseSubtitle": "PodcastHubで簡単にプロフェッショナルなポッドキャストを作成",
"faq": "よくある質問",
"faqSubtitle": "サービス利用に関するよくある質問への回答",
"howToUseSteps": {
"step1": {
"title": "1. コンテンツを入力",
"description": "テキストボックスにポッドキャストに変換したいコンテンツを入力します。記事、メモ、または任意のテキストが使用できます。story generator機能を使用して素早く自動的にコンテンツを生成できます。"
},
"step2": {
"title": "2. モードを選択",
"description": "標準モードまたはAIストーリーモードを選択します。podcast generatorを使用すると、より魅力的なポッドキャストスクリプトを作成できます。"
},
"step3": {
"title": "3. 音声を設定",
"description": "適切な音声ロールを選択して、ポッドキャストの音声をカスタマイズします。"
},
"step4": {
"title": "4. ポッドキャストを生成",
"description": "生成ボタンをクリックすると、PodcastHubが自動的にプロフェッショナルなポッドキャストオーディオを作成します。"
}
},
"faqItems": {
"q1": {
"question": "PodcastHubとは何ですか",
"answer": "PodcastHubは、テキストコンテンツを素早くプロフェッショナルなポッドキャストオーディオに変換するインテリジェントなポッドキャスト生成プラットフォームです。複数の音声とAI生成モードをサポートしています。"
},
"q2": {
"question": "story generator機能の使い方は",
"answer": "AIストーリーモードを選択すると、システムは自動的に没入型ストーリー機能を使用して、生き生きとしたAI音声スクリプトを作成します。"
},
"q3": {
"question": "どの言語がサポートされていますか?",
"answer": "PodcastHubは中国語、英語、日本語など、複数の言語をサポートしています。コンテンツに応じて適切な音声ロールを選択できます。"
},
"q4": {
"question": "生成されたポッドキャストはどのくらい保存されますか?",
"answer": "プライバシーを保護し、ストレージスペースを節約するため、生成されたポッドキャストオーディオは30分間保持されます。時間内にダウンロードしてローカルに保存してください。"
},
"q5": {
"question": "より多くのクレジットを取得するには?",
"answer": "クレジットパッケージを購入することで、より多くの生成回数を取得できます。また、签到や無料体験クレジットを利用することもできます。新規ユーザーは登録時に無料体験クレジットを受け取ります。"
}
},
"dataRetentionWarning": "データは30分間のみ保持されます。できるだけ早くダウンロードして保存してください。",
"saveSuccessTitle": "保存成功",
"saveErrorTitle": "保存失敗",

View File

@@ -1,5 +1,5 @@
{
"title": "PodcastHub:あなたのAIポッドキャスト作成プラットフォーム - テキストを高品質なポッドキャストオーディオに簡単に変換、複数の声とスタイルをサポートし、創造性を手軽に実現。",
"description": "PodcastHubは最先端のAI技術を駆使し、あなたの創造性に無限の可能性を提供します。テキストやアイデアをプロ品質のポッドキャストオーディオに簡単に変換し、多様なパーソナライズされた声とスタイルのオプションを提供します。今すぐ効率的な作成を体験し、あなたの声を世界に広め、より多くのリスナーを引きつけ、ポッドキャスト制作プロセスを簡素化します。",
"title": "PodcastHub: AIポッドキャスト作成 - テキストを音声に変換",
"description": "AIでテキストをプロ品質のポッドキャスト音声に変換多様な声とスタイルで魅力的なコンテンツを簡単作成。今すぐ無料で始めよう!",
"keywords": "ポッドキャスト,AI,音声合成,TTS,オーディオ生成"
}

View File

@@ -18,6 +18,54 @@
"taskCreatedTitle": "任务已创建",
"taskCreatedMessage": "播客生成任务已启动任务ID",
"recentlyGenerated": "最近生成",
"howToUse": "使用说明",
"howToUseSubtitle": "快速上手 PodcastHub,轻松生成专业播客",
"faq": "常见问题",
"faqSubtitle": "解答您在使用过程中可能遇到的问题",
"howToUseSteps": {
"step1": {
"title": "1. 输入内容",
"description": "在文本框中输入您想要转换为播客的内容,可以是文章、笔记或任何文本。支持使用沉浸故事功能快速生成自动配音内容。"
},
"step2": {
"title": "2. 选择模式",
"description": "选择AI播客模式或沉浸故事模式。使用我们的AI播客生成器可以帮助您创建更有吸引力的播客脚本。"
},
"step3": {
"title": "3. 配置语音",
"description": "选择合适的语音角色,自定义您的播客音色。"
},
"step4": {
"title": "4. 生成播客",
"description": "点击生成按钮,PodcastHub 将自动为您创建专业的播客音频。"
}
},
"faqItems": {
"q1": {
"question": "什么是 PodcastHub?",
"answer": "PodcastHub 是一个智能播客生成平台,可以将您的文本内容快速转换为专业的播客音频。支持多种语音和AI生成模式。"
},
"q2": {
"question": "如何使用沉浸故事功能?",
"answer": "选择AI故事模式时,系统会自动使用沉浸故事功能来创建生动的AI配音脚本。"
},
"q3": {
"question": "支持哪些语言?",
"answer": "PodcastHub 支持中文、英文、日文等多种语言。您可以根据内容选择相应的语音角色。"
},
"q4": {
"question": "生成的播客可以保存多久?",
"answer": "为了保护您的隐私和节省存储空间,生成的播客音频会保留30分钟。请及时下载保存到本地。"
},
"q5": {
"question": "如何获取更多积分?",
"answer": "您可以通过签到或购买积分套餐来获取更多生成次数。新用户注册即可获得免费体验积分。"
},
"q6": {
"question": "什么是AI播客生成器?",
"answer": "AI播客生成器是我们的智能播客创作工具,可以帮助您创建更专业、更有吸引力的播客内容。结合沉浸故事功能,在AI故事模式下自动启用,让您的播客更具感染力。"
}
},
"dataRetentionWarning": "数据只保留30分钟请尽快下载保存",
"saveSuccessTitle": "保存成功",
"saveErrorTitle": "保存失败",

View File

@@ -58,7 +58,7 @@ const ContactUsPage: React.FC<{ params: paramsType}> = async ({ params }) => {
<p className="text-gray-600">
{t('email_description')}
<a
href="mailto:support@podcasthub.com"
href="mailto:justlikemaki@foxmail.com"
className="block text-blue-600 hover:text-blue-700 transition-colors break-all mt-1 font-medium"
>
justlikemaki@foxmail.com

View File

@@ -20,10 +20,10 @@ export async function generateMetadata({ params }: { params: { lang: string } })
const { t } = await getTranslation(lang, 'layout');
const truePath = await getTruePathFromHeaders(await headers(), lang, true);
return {
metadataBase: new URL('https://www.podcasthub.com'),
metadataBase: new URL('https://podcast.hubtoday.app'),
title: t('title'),
description: t('description'),
keywords: t('keywords').split(','),
// keywords: t('keywords').split(','),
authors: [{ name: 'PodcastHub Team' }],
icons: {
icon: '/favicon.webp',

View File

@@ -7,6 +7,8 @@ import PodcastCreator from '@/components/PodcastCreator';
import ContentSection from '@/components/ContentSection';
import AudioPlayer from '@/components/AudioPlayer';
import SettingsForm from '@/components/SettingsForm';
import HowToUse from '@/components/HowToUse';
import FAQ from '@/components/FAQ';
import PointsOverview from '@/components/PointsOverview'; // 导入 PointsOverview
import LoginModal from '@/components/LoginModal'; // 导入 LoginModal
import NotificationBanner from '@/components/NotificationBanner'; // 导入 NotificationBanner
@@ -415,6 +417,12 @@ export default function HomePage({ params }: { params: Promise<{ lang: string }>
/>
)}
{/* 使用说明 */}
<HowToUse lang={lang} />
{/* 常见问题 */}
<FAQ lang={lang} />
{/* 定价部分 todo */}
{/* <PricingSection /> */}

View File

@@ -0,0 +1,62 @@
import { Metadata } from 'next';
import { getTranslation } from '../../../../i18n';
import { headers } from 'next/headers';
import { getTruePathFromHeaders } from '../../../../lib/utils';
import { getAudioInfo } from '../../../../lib/podcastApi';
export type paramsType = Promise<{ lang: string, fileName: string }>;
export async function generateMetadata({ params }: { params: paramsType }): Promise<Metadata> {
const { fileName, lang } = await params;
const { t } = await getTranslation(lang);
const decodedFileName = decodeURIComponent(fileName);
// 获取网站主标题
const siteName = 'PodcastHub';
// 获取音频信息以获取 overview_content
const result = await getAudioInfo(decodedFileName, lang);
let pageTitle = `${t('podcastContent.podcastDetails')} - ${decodedFileName}`;
let description = `${t('podcastContent.listenToPodcast')} ${decodedFileName}`;
// 如果成功获取到 overview_content使用它来生成更好的 title 和 description
if (result.success && result.data?.overview_content) {
const overviewContent = result.data.overview_content;
// 从 overview_content 中提取前150个字符作为 description
description = overviewContent.length > 150
? overviewContent.substring(0, 150) + '...'
: overviewContent;
// 尝试从 overview_content 的第一行或前50个字符生成 title
const firstLine = overviewContent.split('\n')[0];
if (firstLine && firstLine.length > 0 && firstLine.length <= 50) {
pageTitle = firstLine;
} else if (overviewContent.length > 0) {
// 如果第一行太长取前50个字符
pageTitle = overviewContent.substring(0, 40) + (overviewContent.length > 40 ? '...' : '');
}
}
// 组合最终的 title: 网站名称 - 页面标题
const title = `${siteName} - ${pageTitle}`;
const truePath = await getTruePathFromHeaders(await headers(), lang);
return {
title,
description,
alternates: {
canonical: `${truePath}/podcast/${decodedFileName}`,
},
};
}
export default function PodcastLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -1,27 +1,7 @@
import { Metadata } from 'next';
import PodcastContent from '@/components/PodcastContent';
import { getTranslation } from '../../../../i18n'; // 导入 getTranslation
import { headers } from 'next/headers';
import { getTruePathFromHeaders } from '../../../../lib/utils';
export type paramsType = Promise<{ lang: string, fileName: string }>;
export async function generateMetadata({ params }: { params: paramsType }): Promise<Metadata> {
const { fileName, lang } = await params;
const { t } = await getTranslation(lang);
const decodedFileName = decodeURIComponent(fileName);
const title = `${t('podcastContent.podcastDetails')} - ${decodedFileName}`;
const description = `${t('podcastContent.listenToPodcast')} ${decodedFileName}`;
const truePath = await getTruePathFromHeaders(await headers(), lang);
return {
title,
description,
alternates: {
canonical: `${truePath}/podcast/${decodedFileName}`,
},
};
}
const PodcastDetailPage: React.FC<{ params: paramsType}> = async ({ params }) => {
const { fileName, lang } = await params; // 解构 lang
return (

108
web/src/components/FAQ.tsx Normal file
View File

@@ -0,0 +1,108 @@
'use client';
import React, { useState } from 'react';
import { useTranslation } from '../i18n/client';
interface FAQProps {
lang: string;
}
export default function FAQ({ lang }: FAQProps) {
const { t } = useTranslation(lang, 'home');
const [openIndex, setOpenIndex] = useState<number | null>(0);
const faqItems = ['q1', 'q2', 'q3', 'q4', 'q5'];
const toggleItem = (index: number) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<section className="w-full max-w-4xl mx-auto px-4 sm:px-6 py-12">
<div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold text-black mb-4">
{t('faq')}
</h2>
<p className="text-lg text-neutral-600">
{t('faqSubtitle')}
</p>
</div>
<div className="space-y-4">
{faqItems.map((item, index) => (
<div
key={item}
className="bg-white border border-neutral-200 rounded-xl overflow-hidden hover:shadow-md transition-shadow duration-300"
>
<button
onClick={() => toggleItem(index)}
className="w-full flex items-center justify-between p-6 text-left focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-inset"
aria-expanded={openIndex === index}
>
<span className="text-lg font-semibold text-black pr-8">
{t(`faqItems.${item}.question`)}
</span>
<svg
className={`w-6 h-6 text-neutral-600 flex-shrink-0 transition-transform duration-300 ${
openIndex === index ? 'transform rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${
openIndex === index ? 'max-h-96' : 'max-h-0'
}`}
>
<div className="px-6 pb-6 text-neutral-600 leading-relaxed">
{t(`faqItems.${item}.answer`)}
</div>
</div>
</div>
))}
</div>
<div className="mt-12 p-6 bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-200 rounded-xl">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-purple-900 mb-2">
{lang === 'zh-CN' && '还有其他问题?'}
{lang === 'en' && 'Have more questions?'}
{lang === 'ja' && '他にご質問はありますか?'}
</h3>
<p className="text-purple-800 mb-4">
{lang === 'zh-CN' && '如果您有任何其他问题或需要帮助,请随时联系我们的支持团队。'}
{lang === 'en' && 'If you have any other questions or need assistance, please feel free to contact our support team.'}
{lang === 'ja' && 'その他のご質問やサポートが必要な場合は、お気軽にサポートチームにお問い合わせください。'}
</p>
<a
href={`/${lang}/contact`}
className="inline-flex items-center gap-2 px-4 py-2 bg-white text-purple-600 font-medium rounded-lg hover:bg-purple-50 transition-colors duration-200"
>
{lang === 'zh-CN' && '联系我们'}
{lang === 'en' && 'Contact Us'}
{lang === 'ja' && 'お問い合わせ'}
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,93 @@
'use client';
import React from 'react';
import { useTranslation } from '../i18n/client';
interface HowToUseProps {
lang: string;
}
export default function HowToUse({ lang }: HowToUseProps) {
const { t } = useTranslation(lang, 'home');
const steps = [
{
key: 'step1',
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
),
},
{
key: 'step2',
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
),
},
{
key: 'step3',
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
),
},
{
key: 'step4',
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
];
return (
<section className="w-full max-w-4xl mx-auto px-4 sm:px-6 py-12">
<div className="text-center mb-10">
<h2 className="text-3xl sm:text-4xl font-bold text-black mb-4">
{t('howToUse')}
</h2>
<p className="text-lg text-neutral-600">
{t('howToUseSubtitle')}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{steps.map((step) => (
<div
key={step.key}
className="flex flex-col items-center text-center p-6 bg-white border border-neutral-200 rounded-xl hover:shadow-lg hover:border-purple-300 transition-all duration-300"
>
<div className="w-14 h-14 flex items-center justify-center bg-gradient-to-br from-purple-500 to-pink-500 text-white rounded-full mb-4 shadow-md">
{step.icon}
</div>
<h3 className="text-lg font-semibold text-black mb-2">
{t(`howToUseSteps.${step.key}.title`)}
</h3>
<p className="text-sm text-neutral-600 leading-relaxed">
{t(`howToUseSteps.${step.key}.description`)}
</p>
</div>
))}
</div>
<div className="mt-10 text-center">
<div className="inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-200 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-300">
<svg className="w-5 h-5 text-purple-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span className="text-sm font-medium text-purple-900">
{lang === 'zh-CN' && '使用 PodcastHub 的AI播客生成器和沉浸故事功能创建更专业的播客'}
{lang === 'en' && 'Use PodcastHub\'s podcast generator and story generator to create more professional podcasts'}
{lang === 'ja' && 'PodcastHubのpodcast generatorとstory generatorでより専門的なポッドキャストを作成'}
</span>
</div>
</div>
</section>
);
}