diff --git a/apps/web/app/api/sitemap/route.ts b/apps/web/app/api/sitemap/route.ts
new file mode 100644
index 00000000..b457ec29
--- /dev/null
+++ b/apps/web/app/api/sitemap/route.ts
@@ -0,0 +1,61 @@
+import { getUriWithOrg } from '@services/config/config';
+import { getOrgCourses } from '@services/courses/courses';
+import { getOrganizationContextInfo } from '@services/organizations/orgs';
+import { NextRequest, NextResponse } from 'next/server';
+
+
+export async function GET(request: NextRequest) {
+ const orgSlug = request.headers.get('X-Sitemap-Orgslug');
+
+ if (!orgSlug) {
+ return NextResponse.json({ error: 'Missing X-Sitemap-Orgslug header' }, { status: 400 });
+ }
+
+ const orgInfo = await getOrganizationContextInfo(orgSlug, null);
+ const courses = await getOrgCourses(orgSlug, null);
+
+ const host = request.headers.get('host');
+ if (!host) {
+ return NextResponse.json({ error: 'Missing host header' }, { status: 400 });
+ }
+
+ const baseUrl = getUriWithOrg(orgSlug, '/');
+
+ const sitemapUrls: SitemapUrl[] = [
+ { loc: baseUrl, priority: 1.0, changefreq: 'daily' },
+ ...courses.map((course: { course_uuid: string }) => ({
+ loc: `${baseUrl}course/${course.course_uuid.replace('course_', '')}`,
+ priority: 0.8,
+ changefreq: 'weekly'
+ }))
+ ];
+
+ const sitemap = generateSitemap(baseUrl, sitemapUrls);
+
+ return new NextResponse(sitemap, {
+ headers: {
+ 'Content-Type': 'application/xml',
+ },
+ });
+}
+
+interface SitemapUrl {
+ loc: string;
+ priority: number;
+ changefreq: string;
+ }
+
+ function generateSitemap(baseUrl: string, urls: SitemapUrl[]): string {
+ const urlEntries = urls.map(({ loc, priority, changefreq }) => `
+
+ ${loc}
+ ${priority.toFixed(1)}
+ ${changefreq}
+ `).join('');
+
+ return `
+
+ ${urlEntries}
+ `;
+ }
+
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 7ce10397..32ce38d6 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -21,6 +21,7 @@ export const config = {
* 5. all root files inside /public (e.g. /favicon.ico)
*/
'/((?!api|_next|fonts|umami|examples|[\\w-]+\\.\\w+).*)',
+ '/sitemap.xml',
],
}
@@ -99,6 +100,29 @@ export default async function middleware(req: NextRequest) {
}
}
+ if (pathname.startsWith('/sitemap.xml')) {
+ let orgslug: string;
+
+ if (hosting_mode === 'multi') {
+ orgslug = fullhost
+ ? fullhost.replace(`.${LEARNHOUSE_DOMAIN}`, '')
+ : (default_org as string);
+ } else {
+ // Single hosting mode
+ orgslug = default_org as string;
+ }
+
+ const sitemapUrl = new URL(`/api/sitemap`, req.url);
+
+ // Create a response object
+ const response = NextResponse.rewrite(sitemapUrl);
+
+ // Set the orgslug in a header
+ response.headers.set('X-Sitemap-Orgslug', orgslug);
+
+ return response;
+ }
+
// Multi Organization Mode
if (hosting_mode === 'multi') {
// Get the organization slug from the URL
diff --git a/apps/web/services/courses/courses.ts b/apps/web/services/courses/courses.ts
index e3f6bd1d..ba6a1e90 100644
--- a/apps/web/services/courses/courses.ts
+++ b/apps/web/services/courses/courses.ts
@@ -12,12 +12,12 @@ import {
*/
export async function getOrgCourses(
- org_id: number,
+ org_slug: string,
next: any,
- access_token: any
+ access_token?: any
) {
const result: any = await fetch(
- `${getAPIUrl()}courses/org_slug/${org_id}/page/1/limit/10`,
+ `${getAPIUrl()}courses/org_slug/${org_slug}/page/1/limit/10`,
RequestBodyWithAuthHeader('GET', null, next, access_token)
)
const res = await errorHandling(result)