Skip to content

Commit 7013885

Browse files
Merge pull request #111 from pranitaurlam/optimize-roadmap-seo
Optimizing Roadmap SEO and Indexing logic
2 parents 9fc2157 + d3a8e2d commit 7013885

6 files changed

Lines changed: 51 additions & 13 deletions

File tree

client/src/components/SEO.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,32 @@ export function SEO({
3939
<title>{fullTitle}</title>
4040
<meta name="description" content={description} />
4141

42-
{/* Open Graph */}
42+
{/* Primary Meta Tags */}
43+
<meta name="title" content={fullTitle} />
44+
45+
{/* Open Graph / Facebook */}
46+
<meta property="og:type" content={ogType} />
47+
<meta property="og:url" content={effectiveCanonical} />
4348
<meta property="og:title" content={fullTitle} />
4449
<meta property="og:description" content={description} />
45-
<meta property="og:type" content={ogType} />
4650
<meta property="og:image" content={absoluteOgImage} />
4751
<meta property="og:site_name" content={SITE_NAME} />
48-
<meta property="og:url" content={effectiveCanonical} />
4952

50-
{/* Twitter Card */}
53+
{/* Twitter */}
5154
<meta name="twitter:card" content="summary_large_image" />
55+
<meta name="twitter:url" content={effectiveCanonical} />
5256
<meta name="twitter:title" content={fullTitle} />
5357
<meta name="twitter:description" content={description} />
5458
<meta name="twitter:image" content={absoluteOgImage} />
5559

56-
{/* Canonical, always present */}
60+
{/* Canonical URL */}
5761
<link rel="canonical" href={effectiveCanonical} />
5862

5963
{/* Robots */}
60-
{noIndex && <meta name="robots" content="noindex,nofollow" />}
64+
<meta
65+
name="robots"
66+
content={noIndex ? "noindex,nofollow" : "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1"}
67+
/>
6168

6269
{/* Structured Data (JSON-LD) */}
6370
{structuredData &&

client/src/module/student/roadmap/RoadmapDetailPage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ export default function RoadmapDetailPage() {
139139
title={`${roadmap.title} (Free)`}
140140
description={roadmap.shortDescription}
141141
canonicalUrl={canonicalUrl(`/roadmaps/${roadmap.slug}`)}
142+
ogImage={roadmap.ogImage || roadmap.coverImage || undefined}
143+
ogType="article"
144+
noIndex={!roadmap.isPublished}
142145
structuredData={faqSchema ? [courseSchema, faqSchema] : courseSchema}
143146
/>
144147

client/src/module/student/roadmap/RoadmapTopicPage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import type {
1919
interface TopicResponse {
2020
topic: RoadmapTopic & {
2121
resources: RoadmapResource[];
22-
section: { slug: string; title: string; orderIndex: number; roadmap: { slug: string; title: string } };
22+
section: {
23+
slug: string;
24+
title: string;
25+
orderIndex: number;
26+
roadmap: { slug: string; title: string; isPublished: boolean };
27+
};
2328
};
2429
}
2530

@@ -117,6 +122,8 @@ export default function RoadmapTopicPage() {
117122
title={`${topic.title} - ${topic.section.roadmap.title}`}
118123
description={topic.summary}
119124
canonicalUrl={canonicalUrl(`/roadmaps/${slug}/topics/${topic.slug}`)}
125+
ogType="article"
126+
noIndex={!topic.section.roadmap.isPublished}
120127
structuredData={learningResourceSchema ?? undefined}
121128
/>
122129

server/src/module/roadmap/roadmap.controller.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ export async function getRoadmap(req: Request, res: Response, next: NextFunction
6363
res.status(404).json({ message: "Roadmap not found" });
6464
return;
6565
}
66+
67+
if (!roadmap.isPublished) {
68+
const user = req.user;
69+
if (!user || (user.role !== "ADMIN" && user.id !== roadmap.ownerUserId)) {
70+
res.status(404).json({ message: "Roadmap not found" });
71+
return;
72+
}
73+
}
74+
6675
res.json({ roadmap });
6776
} catch (err) {
6877
next(err);
@@ -81,12 +90,22 @@ export async function getTopic(req: Request, res: Response, next: NextFunction)
8190
res.status(404).json({ message: "Topic not found" });
8291
return;
8392
}
93+
94+
if (!topic.section.roadmap.isPublished) {
95+
const user = req.user;
96+
if (!user || (user.role !== "ADMIN" && user.id !== topic.section.roadmap.ownerUserId)) {
97+
res.status(404).json({ message: "Topic not found" });
98+
return;
99+
}
100+
}
101+
84102
res.json({ topic });
85103
} catch (err) {
86104
next(err);
87105
}
88106
}
89107

108+
90109
// ─── Auth ──────────────────────────────────────────────────────────────────
91110
export async function enroll(req: Request, res: Response, next: NextFunction) {
92111
try {

server/src/module/roadmap/roadmap.routes.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Router } from "express";
2+
import { authMiddleware, optionalAuthMiddleware } from "../../middleware/auth.middleware.js";
23
import { authMiddleware } from "../../middleware/auth.middleware.js";
34
import { aiRoadmapLimiter } from "../../middleware/rate-limit.middleware.js";
45
import {
@@ -36,8 +37,9 @@ roadmapRouter.post(
3637

3738
// ── Public ────────────────────────────────────────────────────────────────
3839
roadmapRouter.get("/", getRoadmaps);
39-
roadmapRouter.get("/:slug", getRoadmap);
40-
roadmapRouter.get("/:slug/topics/:topicSlug", getTopic);
40+
roadmapRouter.get("/:slug", optionalAuthMiddleware, getRoadmap);
41+
roadmapRouter.get("/:slug/topics/:topicSlug", optionalAuthMiddleware, getTopic);
42+
4143

4244
// ── Auth: enrollment ──────────────────────────────────────────────────────
4345
roadmapRouter.post("/:slug/enroll", authMiddleware, enroll);

server/src/module/roadmap/roadmap.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ export async function listPublishedRoadmaps(opts: {
149149
}
150150

151151
export async function getRoadmapBySlug(slug: string) {
152-
return prisma.roadmap.findFirst({
153-
where: { slug, isPublished: true },
152+
return prisma.roadmap.findUnique({
153+
where: { slug },
154154
include: {
155155
sections: {
156156
orderBy: { orderIndex: "asc" },
@@ -169,7 +169,7 @@ export async function getTopicBySlug(roadmapSlug: string, topicSlug: string) {
169169
return prisma.roadmapTopic.findFirst({
170170
where: {
171171
slug: topicSlug,
172-
section: { roadmap: { slug: roadmapSlug, isPublished: true } },
172+
section: { roadmap: { slug: roadmapSlug } },
173173
},
174174
include: {
175175
resources: { orderBy: { orderIndex: "asc" } },
@@ -178,7 +178,7 @@ export async function getTopicBySlug(roadmapSlug: string, topicSlug: string) {
178178
slug: true,
179179
title: true,
180180
orderIndex: true,
181-
roadmap: { select: { slug: true, title: true } },
181+
roadmap: { select: { slug: true, title: true, isPublished: true, ownerUserId: true } },
182182
},
183183
},
184184
},

0 commit comments

Comments
 (0)