Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Site URL (used for sitemap generation and absolute URLs)
NEXT_PUBLIC_SITE_URL=https://teachlink.app

# Starknet Configuration
NEXT_PUBLIC_STARKNET_NETWORK=goerli-alpha
# NEXT_PUBLIC_STARKNET_RPC_URL=https://your-rpc-endpoint.com
Expand Down
13 changes: 2 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"check-i18n": "node scripts/check-i18n.cjs",
"check-locales": "node scripts/check-locales.mjs",
"prebuild": "npm run check-locales && npm run check-i18n",
"validate": "npm run validate:ui && npm run validate:web3"
"validate": "npm run validate:ui && npm run validate:web3",
"generate:sitemap": "npx tsx scripts/generate-sitemap.ts"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
Expand All @@ -39,10 +40,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
<<<<<<< HEAD
=======
"dompurify": "^3.2.4",
>>>>>>> bc54ed8 (clean: remove node_modules and reset repo)
"framer-motion": "^12.23.0",
"idb": "^8.0.0",
"lucide-react": "^0.462.0",
Expand All @@ -57,20 +55,13 @@
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"react-intersection-observer": "^10.0.3",
<<<<<<< HEAD
=======
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.9",
>>>>>>> bc54ed8 (clean: remove node_modules and reset repo)
"recharts": "^2.15.4",
"socket.io-client": "^4.8.3",
"tailwind-merge": "^2.6.0",
"web-vitals": "^4.2.4",
"workbox-webpack-plugin": "^7.0.0",
<<<<<<< HEAD
"dompurify": "^3.2.4",
=======
>>>>>>> bc54ed8 (clean: remove node_modules and reset repo)
"zod": "^3.25.75",
"zustand": "^5.0.10"
},
Expand Down
94 changes: 94 additions & 0 deletions scripts/generate-sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Standalone sitemap generator — writes public/sitemap.xml at build time.
* Run with: npx tsx scripts/generate-sitemap.ts
*/
import { writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://teachlink.app';

interface SitemapEntry {
url: string;
lastModified?: Date;
changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
priority?: number;
}

const STATIC_ROUTES: SitemapEntry[] = [
{ url: BASE_URL, changeFrequency: 'daily', priority: 1.0 },
{ url: `${BASE_URL}/search`, changeFrequency: 'weekly', priority: 0.8 },
{ url: `${BASE_URL}/study-groups`, changeFrequency: 'weekly', priority: 0.7 },
];

async function fetchAllCourseIds(): Promise<string[]> {
const ids: string[] = [];
let cursor: string | undefined;

try {
do {
const url = new URL(`${BASE_URL}/api/courses`);
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);

const res = await fetch(url.toString());
if (!res.ok) break;

const json = await res.json();
const page: { id: string }[] = Array.isArray(json) ? json : (json.data ?? []);
ids.push(...page.map((c) => c.id));
cursor = json.nextCursor;
} while (cursor);
} catch {
console.warn('Could not fetch courses — only static routes will be included.');
}

return ids;
}

function toXml(entries: SitemapEntry[]): string {
const now = new Date().toISOString().split('T')[0];

const urls = entries
.map(
(entry) => `
<url>
<loc>${entry.url}</loc>
<lastmod>${entry.lastModified ? entry.lastModified.toISOString().split('T')[0] : now}</lastmod>
${entry.changeFrequency ? `<changefreq>${entry.changeFrequency}</changefreq>` : ''}
${entry.priority !== undefined ? `<priority>${entry.priority.toFixed(1)}</priority>` : ''}
</url>`,
)
.join('');

return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`;
}

async function main() {
const courseIds = await fetchAllCourseIds();

const courseRoutes: SitemapEntry[] = courseIds.map((id) => ({
url: `${BASE_URL}/courses/${id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
}));

const allEntries = [...STATIC_ROUTES, ...courseRoutes];
const xml = toXml(allEntries);

const publicDir = join(process.cwd(), 'public');
mkdirSync(publicDir, { recursive: true });

const outputPath = join(publicDir, 'sitemap.xml');
writeFileSync(outputPath, xml, 'utf-8');

console.log(`Sitemap written to ${outputPath} — ${allEntries.length} URL(s) included.`);
}

main().catch((err) => {
console.error('Sitemap generation failed:', err);
process.exit(1);
});
64 changes: 64 additions & 0 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { MetadataRoute } from 'next';
import type { Course, PaginatedResponse } from '@/types/api';

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://teachlink.app';

export const revalidate = 3600; // regenerate every hour

const STATIC_ROUTES: MetadataRoute.Sitemap = [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1.0,
},
{
url: `${BASE_URL}/search`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${BASE_URL}/study-groups`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7,
},
];

async function fetchAllCourses(): Promise<Course[]> {
const courses: Course[] = [];
let cursor: string | undefined;

try {
do {
const url = new URL(`${BASE_URL}/api/courses`);
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);

const res = await fetch(url.toString(), { next: { revalidate: 3600 } });
if (!res.ok) break;

const json: PaginatedResponse<Course> = await res.json();
courses.push(...json.data);
cursor = json.nextCursor;
} while (cursor);
} catch {
// return whatever was collected before the failure
}

return courses;
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const courses = await fetchAllCourses();

const courseRoutes: MetadataRoute.Sitemap = courses.map((course) => ({
url: `${BASE_URL}/courses/${course.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
}));

return [...STATIC_ROUTES, ...courseRoutes];
}
Loading