From 16bc154fb03ca9486211da1f689fe757ee0b3fdd Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Tue, 11 Aug 2026 18:37:43 +0330 Subject: [PATCH] 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 --- .env.example | 1 + apps/admin/src/lib/services.ts | 6 + apps/admin/src/lib/types.ts | 3 + apps/admin/src/pages/BlogNewPage.tsx | 17 +++ apps/admin/src/pages/BrandNewPage.tsx | 17 +++ apps/admin/src/pages/ReportageNewPage.tsx | 17 +++ .../migration.sql | 8 ++ apps/api/prisma/schema.prisma | 3 + apps/api/src/app.ts | 2 + apps/api/src/routes/blogs.ts | 5 + apps/api/src/routes/brands.ts | 5 + apps/api/src/routes/reportages.ts | 5 + apps/api/src/routes/sitemap.ts | 123 ++++++++++++++++++ apps/web/public/robots.txt | 4 + apps/web/src/lib/types.ts | 3 + apps/web/src/lib/usePageMeta.ts | 40 ++++++ apps/web/src/pages/BlogDetailPage.tsx | 6 + apps/web/src/pages/BrandDetailPage.tsx | 6 + apps/web/src/pages/ReportageDetailPage.tsx | 6 + apps/web/vite.config.ts | 4 + 20 files changed, 281 insertions(+) create mode 100644 apps/api/prisma/migrations/20260811150000_meta_description/migration.sql create mode 100644 apps/api/src/routes/sitemap.ts create mode 100644 apps/web/public/robots.txt create mode 100644 apps/web/src/lib/usePageMeta.ts diff --git a/.env.example b/.env.example index 99d258e..3c2295d 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ DATABASE_URL=postgresql://novintrades:novintrades@localhost:5433/novintrades?sch JWT_SECRET=change-me-in-production PORT=3000 HOST=0.0.0.0 +SITE_URL=https://novintrades.com # ParsPack S3-compatible storage S3_ENDPOINT=https://c287211.parspack.net diff --git a/apps/admin/src/lib/services.ts b/apps/admin/src/lib/services.ts index 5fa30fd..48348f3 100644 --- a/apps/admin/src/lib/services.ts +++ b/apps/admin/src/lib/services.ts @@ -32,6 +32,7 @@ export function getBlog(id: string) { export function createBlog(input: { title: string; abstract?: string; + metaDescription?: string; content?: string; imageUrl?: string | null; tags?: string[]; @@ -50,6 +51,7 @@ export function updateBlog( input: { title?: string; abstract?: string; + metaDescription?: string; content?: string; imageUrl?: string | null; tags?: string[]; @@ -76,6 +78,7 @@ export function createReportage(input: { title: string; businessOwner: string; abstract?: string; + metaDescription?: string; content?: string; imageUrl?: string | null; tags?: string[]; @@ -95,6 +98,7 @@ export function updateReportage( title?: string; businessOwner?: string; abstract?: string; + metaDescription?: string; content?: string; imageUrl?: string | null; tags?: string[]; @@ -120,6 +124,7 @@ export function getBrand(id: string) { export function createBrand(input: { title: string; abstract?: string; + metaDescription?: string; content?: string; imageUrl?: string | null; galleryUrls?: string[]; @@ -143,6 +148,7 @@ export function updateBrand( input: { title?: string; abstract?: string; + metaDescription?: string; content?: string; imageUrl?: string | null; galleryUrls?: string[]; diff --git a/apps/admin/src/lib/types.ts b/apps/admin/src/lib/types.ts index 5680510..1fbb21a 100644 --- a/apps/admin/src/lib/types.ts +++ b/apps/admin/src/lib/types.ts @@ -24,6 +24,7 @@ export interface Blog { title: string; slug: string; abstract: string | null; + metaDescription: string | null; content: string; imageUrl: string | null; tags: string[]; @@ -42,6 +43,7 @@ export interface Reportage { slug: string; businessOwner: string; abstract: string | null; + metaDescription: string | null; content: string; imageUrl: string | null; tags: string[]; @@ -71,6 +73,7 @@ export interface Brand { title: string; slug: string; abstract: string | null; + metaDescription: string | null; content: string; imageUrl: string | null; galleryUrls: string[]; diff --git a/apps/admin/src/pages/BlogNewPage.tsx b/apps/admin/src/pages/BlogNewPage.tsx index 3063152..21eac77 100644 --- a/apps/admin/src/pages/BlogNewPage.tsx +++ b/apps/admin/src/pages/BlogNewPage.tsx @@ -28,6 +28,7 @@ export function BlogNewPage() { const [title, setTitle] = useState(""); const [image, setImage] = useState(null); const [abstract, setAbstract] = useState(""); + const [metaDescription, setMetaDescription] = useState(""); const [content, setContent] = useState(""); const [tags, setTags] = useState([]); const [error, setError] = useState(""); @@ -49,6 +50,7 @@ export function BlogNewPage() { if (cancelled) return; setTitle(blog.title); setAbstract(blog.abstract ?? ""); + setMetaDescription(blog.metaDescription ?? ""); setContent(blog.content); setImage(blog.imageUrl); setTags(blog.tags); @@ -86,6 +88,7 @@ export function BlogNewPage() { const payload = { title: title.trim(), abstract: abstract.trim() || undefined, + metaDescription: metaDescription.trim() || undefined, content, imageUrl: image, tags, @@ -175,6 +178,20 @@ export function BlogNewPage() { placeholder="Short summary shown in lists and previews" /> + +
+ +