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..de1e7191 100644
--- a/client/src/screens/landing/index.jsx
+++ b/client/src/screens/landing/index.jsx
@@ -15,7 +15,12 @@ const LandingPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
-
+ useEffect(() => {
+ document.title = "CourseHub | IIT Guwahati Study Materials, Notes & PYQs";
+ return () => {
+ document.title = "CourseHub";
+ };
+}, []);
useEffect(() => {
clearLegacySessionLocalCoursesCache();
}, []);
diff --git a/docs/nginx.conf b/docs/nginx.conf
new file mode 100644
index 00000000..edad911b
--- /dev/null
+++ b/docs/nginx.conf
@@ -0,0 +1,160 @@
+# =============================================================================
+# CourseHub — Nginx Server Block (SEO-Aware Configuration)
+# =============================================================================
+#
+# WHAT CHANGED (vs the old config):
+# NEW location /browse/ → proxied to Express (SEO bot-detection middleware)
+# NEW location /sitemap.xml → proxied to Express (dynamic sitemap — Prince)
+# NEW location /robots.txt → proxied to Express (crawler directives — Prince)
+#
+# HOW TO APPLY:
+# 1. Copy this file to /etc/nginx/sites-available/coursehub
+# 2. sudo ln -s /etc/nginx/sites-available/coursehub /etc/nginx/sites-enabled/
+# 3. sudo nginx -t <- always test before reloading
+# 4. sudo systemctl reload nginx
+#
+# PREREQUISITES:
+# - Express server running on 127.0.0.1:8080
+# - React build output copied to /var/www/coursehub/ (index.html + assets/)
+# - SSL certificate at the paths below (Let's Encrypt / Certbot recommended)
+# =============================================================================
+
+upstream coursehub_express {
+ server 127.0.0.1:8080;
+ keepalive 64;
+}
+
+# -- HTTP -> HTTPS redirect ---------------------------------------------------
+server {
+ listen 80;
+ listen [::]:80;
+ server_name coursehub.codingclub.in;
+
+ # Allow Let's Encrypt ACME challenge through
+ location /.well-known/acme-challenge/ {
+ root /var/www/certbot;
+ }
+
+ location / {
+ return 301 https://$host$request_uri;
+ }
+}
+
+# -- Main HTTPS server block --------------------------------------------------
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name coursehub.codingclub.in;
+
+ # -- SSL ------------------------------------------------------------------
+ ssl_certificate /etc/letsencrypt/live/coursehub.codingclub.in/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/coursehub.codingclub.in/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+
+ # -- Root: React static build ---------------------------------------------
+ root /var/www/coursehub;
+ index index.html;
+
+ # -- Gzip compression -----------------------------------------------------
+ gzip on;
+ gzip_types text/plain text/css application/javascript application/json
+ application/xml image/svg+xml;
+ gzip_min_length 1024;
+
+ # -- Security headers -----------------------------------------------------
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+ # =========================================================================
+ # SEO ROUTES — proxied to Express
+ # =========================================================================
+
+ # [NEW] /browse/* — Express runs the bot-detection SEO middleware here.
+ # Bots -> receive modified HTML with course-specific metadata.
+ # Humans -> Express calls next(), returns the normal React index.html.
+ location /browse/ {
+ proxy_pass http://coursehub_express;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Pass the original User-Agent so the middleware can detect bots
+ proxy_set_header User-Agent $http_user_agent;
+
+ # Timeout safety — MongoDB query should be fast
+ proxy_read_timeout 10s;
+ proxy_connect_timeout 5s;
+ }
+
+ # [NEW] /sitemap.xml — dynamic XML generated from MongoDB (Prince's route)
+ location = /sitemap.xml {
+ proxy_pass http://coursehub_express;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Cache sitemap at Nginx layer for 24 h (Express also sets Cache-Control)
+ proxy_cache_valid 200 24h;
+ }
+
+ # [NEW] /robots.txt — served by Express (Prince's route)
+ location = /robots.txt {
+ proxy_pass http://coursehub_express;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # =========================================================================
+ # API ROUTES — proxied to Express (unchanged from before)
+ # =========================================================================
+ location /api/ {
+ proxy_pass http://coursehub_express;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 30s;
+
+ # Needed for file upload endpoints
+ client_max_body_size 50M;
+ }
+
+ # =========================================================================
+ # STATIC ASSETS — served directly by Nginx (fast path, no Express involved)
+ # =========================================================================
+
+ # Vite build assets: JS/CSS bundles — long-lived cache (content-hashed filenames)
+ location /assets/ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ try_files $uri =404;
+ }
+
+ # Favicon and other root-level static files
+ location ~* \.(ico|png|svg|webp|woff2|woff|ttf)$ {
+ expires 30d;
+ add_header Cache-Control "public";
+ try_files $uri =404;
+ }
+
+ # =========================================================================
+ # SPA FALLBACK — everything else serves index.html (React Router handles it)
+ # =========================================================================
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+}
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..2b54ff31
--- /dev/null
+++ b/server/middleware/seo.js
@@ -0,0 +1,161 @@
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import CourseModel from "../modules/course/course.model.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const BASE_URL = "https://coursehub.codingclub.in";
+
+const INDEX_HTML_PATH =
+ process.env.INDEX_HTML_PATH ||
+ path.resolve(__dirname, "../static/index.html");
+
+let cachedIndexHtml = null;
+
+function getIndexHtml() {
+ if (!cachedIndexHtml) {
+ try {
+ cachedIndexHtml = fs.readFileSync(INDEX_HTML_PATH, "utf-8");
+ } catch {
+ return null;
+ }
+ }
+ return cachedIndexHtml;
+}
+
+const BOT_UA_REGEX =
+ /googlebot|bingbot|slurp|duckduckbot|baiduspider|yandexbot|facebot|facebookexternalhit|twitterbot|discordbot|whatsapp|telegrambot|linkedinbot|slackbot|applebot|ia_archiver|msnbot|ahrefsbot|semrushbot|dotbot|rogerbot|360spider|sogou/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(
+ /
CourseHub<\/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 escapedCode = normalizedCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const flexPattern = escapedCode.split("").join("\\s*");
+
+ const course = await CourseModel.findOne({
+ code: { $regex: new RegExp(`^${flexPattern}$`, "i") },
+ })
+ .select("name code")
+ .lean();
+
+ if (!course) {
+ const safeCode = escapeHtml(normalizedCode);
+ 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..4ba59091
--- /dev/null
+++ b/server/modules/seo/seo.routes.js
@@ -0,0 +1,72 @@
+import express from "express";
+import CourseModel from "../course/course.model.js";
+
+const router = express.Router();
+const BASE_URL = "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) {
+ 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("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
+
+
+
+
+
+