diff --git a/client/public/og-image.png b/client/public/og-image.png
new file mode 100644
index 00000000..e482eb4e
Binary files /dev/null and b/client/public/og-image.png differ
diff --git a/client/src/screens/browse/index.jsx b/client/src/screens/browse/index.jsx
index e1728869..253f866d 100644
--- a/client/src/screens/browse/index.jsx
+++ b/client/src/screens/browse/index.jsx
@@ -78,6 +78,18 @@ const BrowseScreen = () => {
const { code, folderId } = useParams();
const fb = useSelector((state) => state.fileBrowser);
+ useEffect(() => {
+ if (code) {
+ document.title = `${code.toUpperCase()} | CourseHub`;
+ } else {
+ document.title = "CourseHub";
+ }
+
+ return () => {
+ document.title = "CourseHub";
+ };
+ }, [code]);
+
useEffect(() => {
const cleaned = readAllCoursesCache();
if (cleaned.length > 0) {
diff --git a/client/src/screens/error/index.jsx b/client/src/screens/error/index.jsx
index d337bc9b..7558ceb9 100644
--- a/client/src/screens/error/index.jsx
+++ b/client/src/screens/error/index.jsx
@@ -1,6 +1,13 @@
-import React from "react";
+import React, { useEffect } from "react";
const ErrorScreen = () => {
+ useEffect(() => {
+ document.title = "Page Not Found | CourseHub";
+ return () => {
+ document.title = "CourseHub";
+ };
+ }, []);
+
return (
{
);
};
-export default ErrorScreen;
+export default ErrorScreen;
\ No newline at end of file
diff --git a/client/src/screens/landing/index.jsx b/client/src/screens/landing/index.jsx
index 142e4b82..c56ce066 100644
--- a/client/src/screens/landing/index.jsx
+++ b/client/src/screens/landing/index.jsx
@@ -15,7 +15,9 @@ const LandingPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
-
+ useEffect(() => {
+ document.title = "CourseHub";
+ }, []);
useEffect(() => {
clearLegacySessionLocalCoursesCache();
}, []);
diff --git a/server/index.js b/server/index.js
index 9b998c5d..a0d3ab3b 100644
--- a/server/index.js
+++ b/server/index.js
@@ -26,6 +26,8 @@ import fileRoutes from "./modules/file/file.routes.js";
import folderRoutes from "./modules/folder/folder.routes.js";
import yearRoutes from "./modules/year/year.routes.js";
import studentRoutes from "./modules/student/student.routes.js";
+import seoRoutes from "./modules/seo/seo.routes.js";
+import seoMiddleware from "./middleware/seo.js";
const app = express();
const server = http.createServer(app);
@@ -61,6 +63,8 @@ app.use("/api/files", fileRoutes);
app.use("/api/folder", folderRoutes);
app.use("/api/year", yearRoutes);
app.use("/api/student", studentRoutes);
+app.use(seoRoutes);
+
app.use(
"/homepage",
catchAsync(async (req, res) => {
@@ -70,6 +74,10 @@ app.use(
}),
);
+app.use("/browse", seoMiddleware);
+
+app.get("*", (req, res) => res.sendFile(path.resolve(__dirname, "static", "index.html")));
+
app.use((error, req, res, next) => {
logger.error("Unhandled request error", {
error,
@@ -83,7 +91,6 @@ app.use((error, req, res, next) => {
const { status = 500, message = "Something went wrong!" } = error;
return res.status(status).json({ error: true, message });
});
-app.get("*", (req, res) => res.sendFile(path.resolve(__dirname, "static", "index.html")));
async function closeServer() {
if (!server.listening) return;
@@ -129,6 +136,8 @@ process.once("SIGTERM", () => void terminate({ signal: "SIGTERM", exitCode: 0 })
process.once("uncaughtException", (error) => void terminate({ error, exitCode: 1 }));
process.once("unhandledRejection", (error) => void terminate({ error, exitCode: 1 }));
+
+
export async function start() {
await connectDatabase();
scheduler = initScheduler();
diff --git a/server/middleware/seo.js b/server/middleware/seo.js
new file mode 100644
index 00000000..0c6c82ba
--- /dev/null
+++ b/server/middleware/seo.js
@@ -0,0 +1,187 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import CourseModel from "../modules/course/course.model.js";
+import config from "../config/default.js";
+const courseCache = new Map();
+const CACHE_TTL_MS = 60 * 60 * 1000;
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const BASE_URL =
+ config.clientURL || "https://coursehub.codingclub.in";
+
+const INDEX_HTML_PATH =
+ process.env.INDEX_HTML_PATH ||
+ path.resolve(__dirname, "../static/index.html");
+
+let cachedIndexHtml = null;
+let cachedIndexHtmlAt = 0;
+const INDEX_HTML_CACHE_TTL_MS = 10 * 60 * 1000;
+
+function getIndexHtml() {
+ const now = Date.now();
+ if (
+ !cachedIndexHtml ||
+ now - cachedIndexHtmlAt >= INDEX_HTML_CACHE_TTL_MS
+ ) {
+ try {
+ cachedIndexHtml = fs.readFileSync(INDEX_HTML_PATH, "utf-8");
+ cachedIndexHtmlAt = now;
+ } catch {
+ return null;
+ }
+ }
+ return cachedIndexHtml;
+}
+
+const BOT_UA_REGEX =
+ /googlebot|google-inspectiontool|chrome-lighthouse|storebot-google|bingbot|slurp|duckduckbot|baiduspider|yandexbot|facebot|facebookexternalhit|twitterbot|discordbot|whatsapp|telegrambot|linkedinbot|slackbot|applebot|ia_archiver|msnbot|ahrefsbot|semrushbot|dotbot|rogerbot|360spider|sogou|bytespider/i;
+
+function isBot(userAgent) {
+ if (!userAgent) return false;
+ return BOT_UA_REGEX.test(userAgent);
+}
+
+function escapeHtml(str) {
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
+ .replace(//g, ">");
+}
+
+function sanitizeJsonLd(json) {
+ return json.replace(/<\//g, "<\\/");
+}
+
+function injectCourseMetaTags(html, course) {
+ const courseSlug = encodeURIComponent(course.code.replace(/\s+/g, ""));
+ const courseUrl = `${BASE_URL}/browse/${courseSlug}`;
+
+ const safeCode = escapeHtml(course.code);
+ const safeName = escapeHtml(course.name);
+
+ const title = `${safeCode} - ${safeName} Study Materials | CourseHub IIT Guwahati`;
+ const description =
+ `Find past papers, lecture slides, assignments, and notes for ` +
+ `${safeCode} — ${safeName} at IIT Guwahati. ` +
+ `Access all study materials on CourseHub.`;
+
+ const ogImage = `${BASE_URL}/og-image.png`;
+
+ const jsonLd = sanitizeJsonLd(
+ JSON.stringify({
+ "@context": "https://schema.org",
+ "@type": "Course",
+ name: `${course.code} — ${course.name}`,
+ description: `Find past papers, lecture slides, assignments, and notes for ${course.code} — ${course.name} at IIT Guwahati. Access all study materials on CourseHub.`,
+ provider: {
+ "@type": "Organization",
+ name: "CourseHub IIT Guwahati",
+ url: BASE_URL,
+ },
+ url: courseUrl,
+ }),
+ );
+
+ const injectedBlock = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ let modified = html.replace(
+ /
]*>[\s\S]*?<\/title>/i,
+ () => `${title}`,
+ );
+
+ modified = modified.replace(
+ /]*\/?>/i,
+ () => injectedBlock,
+ );
+
+ return modified;
+}
+
+export default async function seoMiddleware(req, res, next) {
+ const userAgent = req.headers["user-agent"] || "";
+
+ if (!isBot(userAgent)) {
+ return next();
+ }
+
+ const match = req.path.match(/^\/([^/]+)/);
+ if (!match) {
+ return next();
+ }
+
+ try {
+ const rawCode = decodeURIComponent(match[1]);
+ const normalizedCode = rawCode.replace(/\s+/g, "");
+ const cacheKey = normalizedCode.toLowerCase();
+ const cachedCourse = courseCache.get(cacheKey);
+ let course;
+
+ if (cachedCourse && cachedCourse.expiresAt > Date.now()) {
+ course = cachedCourse.course;
+ } else {
+ const escapedCode = normalizedCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const flexPattern = escapedCode.split("").join("\\s*");
+
+ course = await CourseModel.findOne({
+ code: { $regex: new RegExp(`^${flexPattern}$`, "i") },
+ })
+ .select("name code")
+ .lean();
+
+ courseCache.set(cacheKey, {
+ course,
+ expiresAt: Date.now() + CACHE_TTL_MS,
+ });
+ }
+
+ if (!course) {
+ const safeCode = escapeHtml(normalizedCode);
+ res.set("Vary", "User-Agent");
+ return res.status(404).send(
+ `
+
+
+
+ 404 — Course Not Found | CourseHub IIT Guwahati
+
+
+
404 — Course Not Found
+
The course ${safeCode} does not exist on CourseHub.
+
Browse all courses
+
+`,
+ );
+ }
+
+ const html = getIndexHtml();
+
+ if (!html) {
+ return next();
+ }
+
+ const modifiedHtml = injectCourseMetaTags(html, course);
+
+ res.set("Vary", "User-Agent");
+ res.set("Cache-Control", "public, max-age=3600, s-maxage=3600");
+ res.set("Content-Type", "text/html; charset=utf-8");
+ return res.send(modifiedHtml);
+ } catch (error) {
+ return next(error);
+ }
+}
diff --git a/server/modules/seo/seo.routes.js b/server/modules/seo/seo.routes.js
new file mode 100644
index 00000000..63b1e757
--- /dev/null
+++ b/server/modules/seo/seo.routes.js
@@ -0,0 +1,74 @@
+import express from "express";
+import CourseModel from "../course/course.model.js";
+import config from "../../config/default.js";
+
+const router = express.Router();
+const BASE_URL = config.clientURL || "https://coursehub.codingclub.in";
+
+// 1 sitemap.xml
+
+router.get("/sitemap.xml", async (req, res) => {
+ try {
+ const courses = await CourseModel.find().select("code updatedAt").lean();
+
+ let xml = `
+
+
+ ${BASE_URL}/
+ weekly
+ 1.0
+
+
+ ${BASE_URL}/browse
+ daily
+ 0.9
+ `;
+
+ for (const course of courses) {
+ if (!course?.code) continue ;
+ const lastmod = course.updatedAt && !isNaN(new Date(course.updatedAt))
+ ? new Date(course.updatedAt).toISOString().split("T")[0]
+ : new Date().toISOString().split("T")[0];
+
+ xml += `
+
+ ${BASE_URL}/browse/${encodeURIComponent(course.code.replace(/\s+/g,""))}
+ ${lastmod}
+ weekly
+ 0.8
+ `;
+ }
+
+ xml += "\n";
+
+ res.set("Cache-Control", "public, max-age=86400, s-maxage=86400")
+ res.set("Content-Type", "application/xml");
+ res.send(xml);
+ } catch (error) {
+ console.error("Error generating sitemap:", error);
+ res.status(500).send("Error generating sitemap");
+ }
+});
+
+// 2 robots.txt
+
+router.get("/robots.txt", (req, res) => {
+ const robotsTxt = `User-agent: *
+Allow: /
+Allow: /browse
+Allow: /browse/
+
+Disallow: /dashboard
+Disallow: /profile
+Disallow: /loading
+Disallow: /api/
+Disallow: /admin/
+
+Sitemap: ${BASE_URL}/sitemap.xml`;
+
+ res.set("Cache-Control", "public, max-age=86400, s-maxage=86400");
+ res.set("Content-Type", "text/plain");
+ res.send(robotsTxt);
+});
+
+export default router;
diff --git a/server/static/index.html b/server/static/index.html
index f5530ddb..e5224697 100644
--- a/server/static/index.html
+++ b/server/static/index.html
@@ -1,11 +1,15 @@
-
-
-
-
Document
-
-
- Server is running
-
-
\ No newline at end of file
+
+
+
+
CourseHub
+
+
+
+
+
+