diff --git a/frontend/app/[lang]/blog/[slug]/metadata.ts b/frontend/app/[lang]/blog/[slug]/metadata.ts index f6ac71e..dc7d141 100644 --- a/frontend/app/[lang]/blog/[slug]/metadata.ts +++ b/frontend/app/[lang]/blog/[slug]/metadata.ts @@ -2,6 +2,8 @@ import { Metadata } from "next"; import { getPostBySlug } from "@/lib/blog"; import { generateMetadata as generateBlogMetadata } from "../metadata"; +import { getDictionary } from "@/lib/dictionary"; +import { supportedLocales } from "@/constants/i18n-config"; export async function generateMetadata({ params, @@ -16,8 +18,12 @@ export async function generateMetadata({ return generateBlogMetadata({ params: { lang: params.lang } }); } + const messages = await getDictionary(params.lang); + const blogWord = messages.text.Header.Blog_dis; + const blogCap = blogWord.charAt(0).toUpperCase() + blogWord.slice(1); + return { - title: `${post.frontmatter.title} | PrivyDrop Blog`, + title: `${post.frontmatter.title} | PrivyDrop ${blogCap}`, description: post.frontmatter.description, keywords: `${post.frontmatter.tags.join( ", " @@ -25,10 +31,9 @@ export async function generateMetadata({ metadataBase: new URL("https://www.privydrop.app"), alternates: { canonical: `/${params.lang}/blog/${params.slug}`, - languages: { - en: `/en/blog/${params.slug}`, - zh: `/zh/blog/${params.slug}`, - }, + languages: Object.fromEntries( + supportedLocales.map((l) => [l, `/${l}/blog/${params.slug}`]) + ), }, openGraph: { title: post.frontmatter.title, diff --git a/frontend/app/[lang]/blog/[slug]/page.tsx b/frontend/app/[lang]/blog/[slug]/page.tsx index 17a633e..62d2ef8 100644 --- a/frontend/app/[lang]/blog/[slug]/page.tsx +++ b/frontend/app/[lang]/blog/[slug]/page.tsx @@ -26,7 +26,7 @@ export default async function BlogPost({ const messages = await getDictionary(params.lang); if (!post) { - return
Post not found
; + return
{messages.text.blog.post_not_found}
; } const siteUrl = getSiteUrl(); @@ -64,7 +64,7 @@ export default async function BlogPost({
· - by {post.frontmatter.author} + {messages.text.blog.by} {post.frontmatter.author}
@@ -92,7 +92,7 @@ export default async function BlogPost({ /> - + ); diff --git a/frontend/app/[lang]/blog/metadata.ts b/frontend/app/[lang]/blog/metadata.ts index d5ecf7f..d944e8b 100644 --- a/frontend/app/[lang]/blog/metadata.ts +++ b/frontend/app/[lang]/blog/metadata.ts @@ -1,29 +1,28 @@ import { supportedLocales } from "@/constants/i18n-config"; import { Metadata } from "next"; +import { getDictionary } from "@/lib/dictionary"; export async function generateMetadata({ params, }: { params: { lang: string }; }): Promise { + const messages = await getDictionary(params.lang); + return { - title: "PrivyDrop Blog - Private P2P File Sharing & Collaboration", - description: - "Discover secure file sharing tips, privacy-focused collaboration strategies, and how to leverage P2P technology for safer data transfer. Learn about WebRTC, end-to-end encryption, and team collaboration.", - keywords: - "secure file sharing, p2p file transfer, private collaboration, webrtc, end-to-end encryption, team collaboration, privacy tools", + title: messages.meta.blog.title, + description: messages.meta.blog.description, + keywords: messages.meta.blog.keywords, metadataBase: new URL("https://www.privydrop.app"), alternates: { canonical: `/${params.lang}/blog`, - languages: { - en: "/en/blog", - zh: "/zh/blog", - }, + languages: Object.fromEntries( + supportedLocales.map((l) => [l, `/${l}/blog`]) + ), }, openGraph: { - title: "PrivyDrop Blog - Private P2P File Sharing & Collaboration", - description: - "Explore secure file sharing, private collaboration tools, and data privacy best practices. Join our community of privacy-conscious professionals.", + title: messages.meta.blog.title, + description: messages.meta.blog.description, url: `https://www.privydrop.app/${params.lang}/blog`, siteName: "PrivyDrop", locale: params.lang, diff --git a/frontend/app/[lang]/blog/page.tsx b/frontend/app/[lang]/blog/page.tsx index f4a8081..ba7389b 100644 --- a/frontend/app/[lang]/blog/page.tsx +++ b/frontend/app/[lang]/blog/page.tsx @@ -3,6 +3,7 @@ import { ArticleListItem } from "@/components/blog/ArticleListItem"; import Link from "next/link"; import { slugifyTag } from "@/utils/tagUtils"; import { generateMetadata } from "./metadata"; +import { getDictionary } from "@/lib/dictionary"; export { generateMetadata }; @@ -12,6 +13,7 @@ export default async function BlogPage({ params: { lang: string }; }) { const posts = await getAllPosts(lang); + const messages = await getDictionary(lang); return (
@@ -19,14 +21,14 @@ export default async function BlogPage({ {/* Main Content */}
-

Blog

-

Latest articles and updates

+

{messages.text.blog.list_title}

+

{messages.text.blog.list_subtitle}

{/* Articles List */}
{posts.map((post) => ( - + ))}
@@ -36,7 +38,7 @@ export default async function BlogPage({
{/* Recent Posts */}
-

Recent Posts

+

{messages.text.blog.recent_posts}

{posts.slice(0, 5).map((post) => ( {/* tags */}
-

Tags

+

{messages.text.blog.tags}

{/* Get all tags and deduplicate */} {Array.from( diff --git a/frontend/app/[lang]/blog/tag/[tag]/page.tsx b/frontend/app/[lang]/blog/tag/[tag]/page.tsx index 43fba35..4ef13d2 100644 --- a/frontend/app/[lang]/blog/tag/[tag]/page.tsx +++ b/frontend/app/[lang]/blog/tag/[tag]/page.tsx @@ -3,6 +3,7 @@ import { getPostsByTag } from "@/lib/blog"; import { ArticleListItem } from "@/components/blog/ArticleListItem"; import { supportedLocales } from "@/constants/i18n-config"; import { unslugifyTag } from "@/utils/tagUtils"; +import { getDictionary } from "@/lib/dictionary"; export async function generateMetadata({ params: { tag, lang }, @@ -10,25 +11,24 @@ export async function generateMetadata({ params: { tag: string; lang: string }; }): Promise { const decodedTag = unslugifyTag(tag); + const messages = await getDictionary(lang); + // Note: metadata text kept concise and localized return { - title: `${decodedTag} - PrivyDrop Blog Articles`, - description: `Explore articles about ${decodedTag} - Learn about secure file sharing, private collaboration, and data privacy solutions related to ${decodedTag}`, - keywords: `${decodedTag}, secure file sharing, p2p file transfer, privacy, collaboration, webrtc`, + title: `${messages.text.blog.tag_title_prefix}: ${decodedTag} - PrivyDrop`, + description: messages.text.blog.tag_subtitle_template.replace("{tag}", decodedTag), + keywords: `${decodedTag}, blog, privydrop`, metadataBase: new URL("https://www.privydrop.app"), alternates: { canonical: `/${lang}/blog/tag/${encodeURIComponent(tag)}`, - languages: { - en: `/en/blog/tag/${encodeURIComponent(tag)}`, - zh: `/zh/blog/tag/${encodeURIComponent(tag)}`, - }, + languages: Object.fromEntries( + supportedLocales.map((l) => [l, `/${l}/blog/tag/${encodeURIComponent(tag)}`]) + ), }, openGraph: { - title: `${decodedTag} - PrivyDrop Blog Articles`, - description: `Discover articles about ${decodedTag} - Expert insights on secure file sharing and private collaboration solutions`, - url: `https://www.privydrop.app/${lang}/blog/tag/${encodeURIComponent( - tag - )}`, + title: `${decodedTag} - PrivyDrop`, + description: `Articles tagged: ${decodedTag}`, + url: `https://www.privydrop.app/${lang}/blog/tag/${encodeURIComponent(tag)}`, siteName: "PrivyDrop", locale: lang, type: "website", @@ -42,6 +42,7 @@ export default async function TagPage({ }) { const decodedTag = unslugifyTag(tag); const posts = await getPostsByTag(decodedTag, lang); + const messages = await getDictionary(lang); return (
@@ -49,9 +50,9 @@ export default async function TagPage({ {/* Main Content */}
-

Tag: {decodedTag}

+

{messages.text.blog.tag_title_prefix}: {decodedTag}

- Articles tagged with {decodedTag} + {messages.text.blog.tag_subtitle_template.replace("{tag}", decodedTag)}

@@ -59,10 +60,10 @@ export default async function TagPage({
{posts.length > 0 ? ( posts.map((post) => ( - + )) ) : ( -

No articles found for this decodedTag.

+

{messages.text.blog.tag_empty}

)}
diff --git a/frontend/app/sitemap.ts b/frontend/app/sitemap.ts index 845fa9e..c512b63 100644 --- a/frontend/app/sitemap.ts +++ b/frontend/app/sitemap.ts @@ -1,6 +1,7 @@ import { MetadataRoute } from "next"; import { supportedLocales } from "@/constants/i18n-config"; import { getAllPosts } from "@/lib/blog"; +import { slugifyTag } from "@/utils/tagUtils"; export default async function sitemap(): Promise { const baseUrl = "https://www.privydrop.app"; @@ -26,23 +27,33 @@ export default async function sitemap(): Promise { priority: 1, }); - // Add language specific URLs - languages.forEach((lang) => { - routes.forEach((route) => { - urls.push({ - url: `${baseUrl}/${lang}${route}`, - lastModified: new Date(), - changeFrequency: "weekly", - priority: route === "" ? 1.0 : 0.8, - }); - }); - }); - - // Add blog posts for each language + // Add language specific URLs, blog posts and tag pages for (const lang of languages) { try { const posts = await getAllPosts(lang); - + + // compute latest blog post date for this language + const latestDate = posts.length + ? new Date( + Math.max( + ...posts.map((p) => new Date(p.frontmatter.date).getTime()) + ) + ) + : new Date(); + + // Add static routes per language (optimize blog list lastModified) + routes.forEach((route) => { + const isRoot = route === ""; + const isBlogList = route === "/blog"; + urls.push({ + url: `${baseUrl}/${lang}${route}`, + lastModified: isBlogList ? latestDate : new Date(), + changeFrequency: isRoot ? "weekly" : isBlogList ? "weekly" : "weekly", + priority: isRoot ? 1.0 : 0.8, + }); + }); + + // Add blog posts for this language posts.forEach((post) => { urls.push({ url: `${baseUrl}/${lang}/blog/${post.slug}`, @@ -51,8 +62,38 @@ export default async function sitemap(): Promise { priority: 0.7, }); }); + + // Add tag pages for this language + const uniqueTags = Array.from( + new Set(posts.flatMap((p) => p.frontmatter.tags)) + ); + uniqueTags.forEach((tag) => { + const tagSlug = slugifyTag(tag); + const tagLatestDate = posts + .filter((p) => p.frontmatter.tags.includes(tag)) + .map((p) => new Date(p.frontmatter.date).getTime()); + const lastModified = + tagLatestDate.length > 0 + ? new Date(Math.max(...tagLatestDate)) + : latestDate; + urls.push({ + url: `${baseUrl}/${lang}/blog/tag/${tagSlug}`, + lastModified, + changeFrequency: "monthly", + priority: 0.6, + }); + }); } catch (error) { - console.warn(`Failed to load blog posts for language ${lang}:`, error); + console.warn(`Failed to load blog data for language ${lang}:`, error); + // Fallback: keep at least the static routes + routes.forEach((route) => { + urls.push({ + url: `${baseUrl}/${lang}${route}`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: route === "" ? 1.0 : 0.8, + }); + }); } } diff --git a/frontend/components/blog/ArticleListItem.tsx b/frontend/components/blog/ArticleListItem.tsx index db7bab2..84c8fa1 100644 --- a/frontend/components/blog/ArticleListItem.tsx +++ b/frontend/components/blog/ArticleListItem.tsx @@ -1,13 +1,15 @@ import Link from "next/link"; import Image from "next/image"; import { type BlogPost } from "@/lib/blog"; +import { Messages } from "@/types/messages"; interface ArticleListItemProps { post: BlogPost; lang: string; + messages: Messages; } -export function ArticleListItem({ post, lang }: ArticleListItemProps) { +export function ArticleListItem({ post, lang, messages }: ArticleListItemProps) { return (
@@ -24,7 +26,11 @@ export function ArticleListItem({ post, lang }: ArticleListItemProps) {
·
@@ -53,7 +59,7 @@ export function ArticleListItem({ post, lang }: ArticleListItemProps) { href={`/${lang}/blog/${post.slug}`} className="text-blue-600 hover:text-blue-800 font-medium inline-flex items-center text-lg" > - Read more + {messages.text.blog.read_more} - by {post.frontmatter.author} + {messages.text.blog.by} {post.frontmatter.author}
diff --git a/frontend/components/blog/TableOfContents.tsx b/frontend/components/blog/TableOfContents.tsx index ae50069..21f4772 100644 --- a/frontend/components/blog/TableOfContents.tsx +++ b/frontend/components/blog/TableOfContents.tsx @@ -10,10 +10,12 @@ interface TocItem { interface TableOfContentsProps { content: string; + title?: string; } export const TableOfContents: React.FC = ({ content, + title = "Table of contents", }) => { const [activeId, setActiveId] = useState(""); const [toc, setToc] = useState([]); @@ -110,7 +112,7 @@ export const TableOfContents: React.FC = ({ return (