mirror of
https://git.meshkee.com/novintrades/website.git
synced 2026-08-12 05:00:58 +04:30
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:
co-authored by
Cursor
parent
5088e0f4ae
commit
16bc154fb0
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://novintrades.com/sitemap.xml
|
||||
@@ -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[];
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user