Add SEO meta descriptions and a dynamic sitemap.

Expose optional metaDescription for blogs, reportages, and brands in admin/API, apply it on detail pages, and serve an up-to-date /sitemap.xml for approved content plus static site pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-11 18:37:43 +03:30
co-authored by Cursor
parent 5088e0f4ae
commit 16bc154fb0
20 changed files with 281 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://novintrades.com/sitemap.xml
+3
View File
@@ -18,6 +18,7 @@ export interface Blog {
title: string;
slug: string;
abstract: string | null;
metaDescription: string | null;
content: string;
imageUrl: string | null;
tags: string[];
@@ -34,6 +35,7 @@ export interface Reportage {
slug: string;
businessOwner: string;
abstract: string | null;
metaDescription: string | null;
content: string;
imageUrl: string | null;
tags: string[];
@@ -54,6 +56,7 @@ export interface Brand {
title: string;
slug: string;
abstract: string | null;
metaDescription: string | null;
content: string;
imageUrl: string | null;
galleryUrls: string[];
+40
View File
@@ -0,0 +1,40 @@
import { useEffect } from "react";
const DEFAULT_TITLE = "NovinTrades";
const DEFAULT_DESCRIPTION =
"NovinTrades — discover brands, insights, and trade opportunities across industries.";
function getOrCreateMetaDescription(): HTMLMetaElement {
const existing = document.querySelector<HTMLMetaElement>(
'meta[name="description"]',
);
if (existing) return existing;
const meta = document.createElement("meta");
meta.name = "description";
document.head.appendChild(meta);
return meta;
}
export function usePageMeta(options: {
title?: string | null;
description?: string | null;
}) {
const { title, description } = options;
useEffect(() => {
const previousTitle = document.title;
const meta = getOrCreateMetaDescription();
const previousDescription = meta.content;
document.title = title?.trim()
? `${title.trim()} | NovinTrades`
: DEFAULT_TITLE;
meta.content = description?.trim() || DEFAULT_DESCRIPTION;
return () => {
document.title = previousTitle;
meta.content = previousDescription;
};
}, [title, description]);
}
+6
View File
@@ -10,6 +10,7 @@ import {
fetchBlogBySlug,
} from "../lib/services";
import type { Blog, Brand } from "../lib/types";
import { usePageMeta } from "../lib/usePageMeta";
import "./detail.css";
export default function BlogDetailPage() {
@@ -20,6 +21,11 @@ export default function BlogDetailPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
usePageMeta({
title: blog?.title,
description: blog?.metaDescription || blog?.abstract,
});
useEffect(() => {
let active = true;
setLoading(true);
+6
View File
@@ -9,6 +9,7 @@ import {
fetchBrandBySlug,
} from "../lib/services";
import type { Brand } from "../lib/types";
import { usePageMeta } from "../lib/usePageMeta";
import "./detail.css";
export default function BrandDetailPage() {
@@ -18,6 +19,11 @@ export default function BrandDetailPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
usePageMeta({
title: brand?.title,
description: brand?.metaDescription || brand?.abstract,
});
useEffect(() => {
let active = true;
setLoading(true);
@@ -9,6 +9,7 @@ import {
fetchReportageBySlug,
} from "../lib/services";
import type { Blog, Reportage } from "../lib/types";
import { usePageMeta } from "../lib/usePageMeta";
import "./detail.css";
export default function ReportageDetailPage() {
@@ -18,6 +19,11 @@ export default function ReportageDetailPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
usePageMeta({
title: reportage?.title,
description: reportage?.metaDescription || reportage?.abstract,
});
useEffect(() => {
let active = true;
setLoading(true);
+4
View File
@@ -17,6 +17,10 @@ export default defineConfig({
target: "http://127.0.0.1:3000",
changeOrigin: true,
},
"/sitemap.xml": {
target: "http://127.0.0.1:3000",
changeOrigin: true,
},
},
},
});