Skip to content

Commit b2c6b79

Browse files
authored
Merge pull request #191 from IamOluwatoyin/feat/seo-metadata-overhaul
feat: overhaul SEO metadata with structured data, dynamic OG cards, a…
2 parents 307b829 + 9cd1660 commit b2c6b79

23 files changed

Lines changed: 767 additions & 37 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# ─── Site ──────────────────────────────────────────
2+
NEXT_PUBLIC_SITE_URL=https://deenbridge.com
3+
# Canonical origin. Used as metadataBase, and for absolute Open Graph, sitemap.xml
4+
# and robots.txt URLs. Falls back to the Vercel deployment URL, then localhost.
5+
16
# ─── API ───────────────────────────────────────────
27
NEXT_PUBLIC_API_URL=http://localhost:5000
38
# Base URL of the dnb-backend REST API

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
# production
2121
/build
22+
/build.log
2223

2324
# misc
2425
.DS_Store

app/[locale]/(pages)/(auth)/login/page.jsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ import { GalleryVerticalEnd } from 'lucide-react'
44
import React from 'react'
55
import Image from "next/image"
66
import Link from 'next/link'
7+
import { siteUrl, siteName } from "@/lib/config/site.config"
8+
9+
export const metadata = {
10+
title: { absolute: "Sign in | Deen Bridge" },
11+
description:
12+
"Sign in to your Deen Bridge account to access your courses, books, community spaces, and AI assistant.",
13+
alternates: { canonical: "/login" },
14+
openGraph: {
15+
title: "Sign in | Deen Bridge",
16+
description:
17+
"Sign in to your Deen Bridge account to access your courses, books, community spaces, and AI assistant.",
18+
url: `${siteUrl}/login`,
19+
siteName,
20+
locale: "en_US",
21+
type: "website",
22+
images: [{ url: "/og.png", width: 1200, height: 630, alt: "Deen Bridge" }],
23+
},
24+
twitter: {
25+
card: "summary_large_image",
26+
title: "Sign in | Deen Bridge",
27+
description:
28+
"Sign in to your Deen Bridge account to access your courses, books, community spaces, and AI assistant.",
29+
images: ["/og.png"],
30+
},
31+
}
732
const page = () => {
833
return (
934
<div className="grid min-h-svh lg:grid-cols-2">

app/[locale]/(pages)/(auth)/signup/page.jsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@ import { GalleryVerticalEnd } from "lucide-react"
22
import { SignupForm } from "@/components/organisms/auth/signup-form"
33
import Image from "next/image"
44
import Link from "next/link"
5+
import { siteUrl, siteName } from "@/lib/config/site.config"
6+
7+
export const metadata = {
8+
title: { absolute: "Create account | Deen Bridge" },
9+
description:
10+
"Create your free Deen Bridge account and start learning the Qur'an, Arabic, fiqh, and more from verified Islamic educators.",
11+
alternates: { canonical: "/signup" },
12+
openGraph: {
13+
title: "Create account | Deen Bridge",
14+
description:
15+
"Create your free Deen Bridge account and start learning the Qur'an, Arabic, fiqh, and more from verified Islamic educators.",
16+
url: `${siteUrl}/signup`,
17+
siteName,
18+
locale: "en_US",
19+
type: "website",
20+
images: [{ url: "/og.png", width: 1200, height: 630, alt: "Deen Bridge" }],
21+
},
22+
twitter: {
23+
card: "summary_large_image",
24+
title: "Create account | Deen Bridge",
25+
description:
26+
"Create your free Deen Bridge account and start learning the Qur'an, Arabic, fiqh, and more from verified Islamic educators.",
27+
images: ["/og.png"],
28+
},
29+
}
530
export default function SignupPage() {
631
return (
732
<div className="grid min-h-svh lg:grid-cols-2">

app/[locale]/(pages)/blog/layout.jsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import Navbar from "@/components/molecules/ladingpage/Navbar";
2+
import { siteUrl, siteName } from "@/lib/config/site.config";
23

34
export const metadata = {
4-
title: "Deen Bridge Blog | Islamic Learning, Qur'an, Arabic, Fiqh & More",
5+
title: { absolute: "Deen Bridge Blog | Islamic Learning, Qur'an, Arabic, Fiqh & More" },
56
description:
67
"Explore the Deen Bridge Blog for authentic Islamic knowledge, tips, and stories. Learn Qur'an, Arabic, Fiqh, and more through expert articles and community insights.",
78
keywords: [
@@ -20,30 +21,30 @@ export const metadata = {
2021
title: "Deen Bridge Blog | Islamic Learning, Qur'an, Arabic, Fiqh & More",
2122
description:
2223
"Explore the Deen Bridge Blog for authentic Islamic knowledge, tips, and stories. Learn Qur'an, Arabic, Fiqh, and more through expert articles and community insights.",
23-
url: "https://deenbridge.com/blog",
24+
url: `${siteUrl}/blog`,
2425
type: "website",
2526
images: [
2627
{
27-
url: "/favicon.ico",
28+
url: "/og.png",
2829
width: 1200,
2930
height: 630,
3031
alt: "Deen Bridge - Islamic Learning Platform",
3132
},
3233
],
33-
site_name: "Deen Bridge Blog",
34+
site_name: siteName,
3435
},
3536
twitter: {
3637
card: "summary_large_image",
3738
title: "Deen Bridge Blog | Islamic Learning, Qur'an, Arabic, Fiqh & More",
3839
description:
3940
"Explore the Deen Bridge Blog for authentic Islamic knowledge, tips, and stories. Learn Qur'an, Arabic, Fiqh, and more through expert articles and community insights.",
40-
images: ["/favicon.ico"],
41-
site: "@deenbridge",
42-
creator: "@deenbridge"
41+
images: ["/og.png"],
42+
site: "@deen_bridge",
43+
creator: "@deen_bridge"
4344
},
44-
authors: [{ name: "Deen Bridge Team", url: "https://deenbridge.com/about" }],
45+
authors: [{ name: "Deen Bridge Team", url: `${siteUrl}/about` }],
4546
creator: "Deen Bridge Team",
46-
publisher: "Deen Bridge",
47+
publisher: siteName,
4748
robots: "index, follow",
4849
};
4950

app/[locale]/(pages)/educators/[profileid]/EducatorPageClient.jsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ import {
3333
} from "@/lib/config/font.config";
3434
import Button from "@/components/atoms/form/Button";
3535

36-
export default function PublicEducatorPage({ params }) {
36+
export default function PublicEducatorPage({ params, educator: educatorProp }) {
3737
const { profileid } = use(params);
3838
const { user: currentUser } = useAuth();
3939
const router = useRouter();
4040

41-
const [educator, setEducator] = useState(null);
41+
const [educator, setEducator] = useState(educatorProp || null);
4242
const [courses, setCourses] = useState([]);
4343
const [books, setBooks] = useState([]);
4444
const [spaces, setSpaces] = useState([]);
@@ -54,8 +54,11 @@ export default function PublicEducatorPage({ params }) {
5454
setLoading(true);
5555
setError(false);
5656
try {
57-
const res = await getUserById(profileid);
58-
const u = res?.user || null;
57+
let u = educatorProp;
58+
if (!u) {
59+
const res = await getUserById(profileid);
60+
u = res?.user || null;
61+
}
5962
if (!u) {
6063
setError(true);
6164
setLoading(false);
@@ -100,7 +103,7 @@ export default function PublicEducatorPage({ params }) {
100103
}
101104
}
102105
load();
103-
}, [profileid, currentUser?._id]);
106+
}, [profileid, educatorProp, currentUser?._id]);
104107

105108
const handleFollowToggle = async () => {
106109
if (!currentUser?._id) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { getUserById } from "@/lib/actions/users/getUserById";
2+
import {
3+
renderOgCard,
4+
OG_IMAGE_SIZE,
5+
} from "@/components/seo/renderOgCard";
6+
import { truncateText } from "@/lib/utils/seo";
7+
8+
export const runtime = "nodejs";
9+
export const alt = "Educator on Deen Bridge";
10+
export const size = OG_IMAGE_SIZE;
11+
export const contentType = "image/png";
12+
13+
export default async function Image({ params }) {
14+
const { profileid } = await params;
15+
16+
let educator = null;
17+
try {
18+
const res = await getUserById(profileid);
19+
educator = res?.user || null;
20+
} catch {
21+
educator = null;
22+
}
23+
24+
const title = educator?.name || "Meet an educator on Deen Bridge";
25+
const subtitle =
26+
truncateText(educator?.bio, 140) ||
27+
"Courses, books and live spaces from a verified educator — on Deen Bridge.";
28+
const badge = educator?.isVerified
29+
? "Verified Educator"
30+
: "Educator on Deen Bridge";
31+
32+
return renderOgCard({ title, subtitle, badge, typeLabel: "EDUCATOR" });
33+
}
Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,81 @@
1+
import { cache } from "react";
2+
import { notFound } from "next/navigation";
3+
import { getUserById } from "@/lib/actions/users/getUserById";
14
import EducatorPageClient from "./EducatorPageClient";
5+
import { JsonLd } from "@/components/seo/JsonLd";
6+
import { siteUrl, siteName } from "@/lib/config/site.config";
7+
import { truncateText } from "@/lib/utils/seo";
8+
9+
// getUserById uses axios (not fetch), so React cache() deduplicates the call
10+
// between generateMetadata and the page, keeping it to one backend hit.
11+
const getEducator = cache(getUserById);
12+
13+
async function resolveEducator(profileid) {
14+
const res = await getEducator(profileid);
15+
return res?.user || null;
16+
}
217

318
export async function generateMetadata({ params }) {
419
const { profileid } = await params;
20+
const educator = await resolveEducator(profileid);
21+
if (!educator) return {};
22+
23+
const name = educator.name || "Educator";
24+
const title = `${name} | Deen Bridge`;
25+
const description =
26+
truncateText(educator.bio, 160) ||
27+
`Learn from ${name} — courses, books and live spaces on Deen Bridge.`;
28+
const nameParts = name.split(/\s+/).filter(Boolean);
29+
530
return {
6-
title: "Educator Profile - Deen Bridge",
7-
description: "View this educator's courses, books, and spaces on Deen Bridge.",
31+
title: { absolute: title },
32+
description,
33+
alternates: { canonical: `/educators/${profileid}` },
834
openGraph: {
9-
title: "Educator Profile - Deen Bridge",
10-
description: "View this educator's courses, books, and spaces on Deen Bridge.",
11-
url: `https://deenbridge.com/educators/${profileid}`,
35+
title,
36+
description,
37+
url: `${siteUrl}/educators/${profileid}`,
38+
siteName,
39+
locale: "en_US",
1240
type: "profile",
41+
profile: {
42+
firstName: nameParts[0] || undefined,
43+
lastName: nameParts.slice(1).join(" ") || undefined,
44+
username: profileid,
45+
},
1346
},
1447
twitter: {
1548
card: "summary_large_image",
16-
title: "Educator Profile - Deen Bridge",
17-
description: "View this educator's courses, books, and spaces on Deen Bridge.",
49+
title,
50+
description,
1851
},
1952
};
2053
}
2154

22-
export default function EducatorPage({ params }) {
23-
return <EducatorPageClient params={params} />;
55+
function buildProfileJsonLd(educator, profileid) {
56+
return {
57+
"@context": "https://schema.org",
58+
"@type": "ProfilePage",
59+
mainEntity: {
60+
"@type": "Person",
61+
name: educator.name,
62+
url: `${siteUrl}/educators/${profileid}`,
63+
description: truncateText(educator.bio, 200) || undefined,
64+
image: educator.avatar || undefined,
65+
},
66+
};
2467
}
68+
69+
export default async function EducatorPage({ params }) {
70+
const { profileid } = await params;
71+
const educator = await resolveEducator(profileid);
72+
73+
if (!educator) return notFound();
74+
75+
return (
76+
<>
77+
<JsonLd data={buildProfileJsonLd(educator, profileid)} />
78+
<EducatorPageClient params={params} educator={educator} />
79+
</>
80+
);
81+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { getUserById } from "@/lib/actions/users/getUserById";
2+
import {
3+
renderOgCard,
4+
OG_IMAGE_SIZE,
5+
} from "@/components/seo/renderOgCard";
6+
import { truncateText } from "@/lib/utils/seo";
7+
8+
export const runtime = "nodejs";
9+
export const alt = "Educator on Deen Bridge";
10+
export const size = OG_IMAGE_SIZE;
11+
export const contentType = "image/png";
12+
13+
export default async function Image({ params }) {
14+
const { profileid } = await params;
15+
16+
let educator = null;
17+
try {
18+
const res = await getUserById(profileid);
19+
educator = res?.user || null;
20+
} catch {
21+
educator = null;
22+
}
23+
24+
const title = educator?.name || "Meet an educator on Deen Bridge";
25+
const subtitle =
26+
truncateText(educator?.bio, 140) ||
27+
"Courses, books and live spaces from a verified educator — on Deen Bridge.";
28+
const badge = educator?.isVerified
29+
? "Verified Educator"
30+
: "Educator on Deen Bridge";
31+
32+
return renderOgCard({ title, subtitle, badge, typeLabel: "EDUCATOR" });
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { getCourseById } from "@/lib/actions/courses/get-course";
2+
import {
3+
renderOgCard,
4+
OG_IMAGE_SIZE,
5+
} from "@/components/seo/renderOgCard";
6+
7+
export const runtime = "nodejs";
8+
export const alt = "Deen Bridge course";
9+
export const size = OG_IMAGE_SIZE;
10+
export const contentType = "image/png";
11+
12+
export default async function Image({ params }) {
13+
const { courseId } = await params;
14+
15+
let course = null;
16+
try {
17+
course = await getCourseById(courseId);
18+
} catch {
19+
course = null;
20+
}
21+
22+
const title = course?.title || "Learn authentic Islamic knowledge";
23+
const subtitle = course?.createdBy?.name
24+
? `Taught by ${course.createdBy.name}`
25+
: "Courses on Qur'an, Arabic, fiqh and more — on Deen Bridge.";
26+
const badge = course
27+
? course.price === 0
28+
? "Free"
29+
: `$${course.price} USDC`
30+
: "Deen Bridge";
31+
32+
return renderOgCard({ title, subtitle, badge });
33+
}

0 commit comments

Comments
 (0)