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}
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 (