Complete Next.js migration with SSR and Docker deployment
- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router - Add server-side rendering for all pages (Home, Compare, Rankings) - Create individual school pages with dynamic routing (/school/[urn]) - Implement Chart.js and Leaflet map integrations - Add comprehensive SEO with sitemap, robots.txt, and JSON-LD - Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js) - Update CI/CD pipeline to build both backend and frontend images - Fix Dockerfile to include devDependencies for TypeScript compilation - Add Jest testing configuration - Implement performance optimizations (code splitting, caching) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
57
nextjs-app/app/compare/page.tsx
Normal file
57
nextjs-app/app/compare/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Compare Page (SSR)
|
||||
* Side-by-side comparison of schools with metrics
|
||||
*/
|
||||
|
||||
import { fetchComparison, fetchMetrics } from '@/lib/api';
|
||||
import { ComparisonView } from '@/components/ComparisonView';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
interface ComparePageProps {
|
||||
searchParams: Promise<{
|
||||
urns?: string;
|
||||
metric?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Compare Schools',
|
||||
description: 'Compare KS2 performance across multiple primary schools in England',
|
||||
keywords: 'school comparison, compare schools, KS2 comparison, primary school performance',
|
||||
};
|
||||
|
||||
// Force dynamic rendering
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function ComparePage({ searchParams }: ComparePageProps) {
|
||||
const { urns: urnsParam, metric: metricParam } = await searchParams;
|
||||
|
||||
const urns = urnsParam?.split(',').map(Number).filter(Boolean) || [];
|
||||
const selectedMetric = metricParam || 'rwm_expected_pct';
|
||||
|
||||
// Fetch comparison data if URNs provided
|
||||
let comparisonData = null;
|
||||
if (urns.length > 0) {
|
||||
try {
|
||||
const response = await fetchComparison(urnsParam!);
|
||||
comparisonData = response.comparison;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch comparison:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch available metrics
|
||||
const metricsResponse = await fetchMetrics();
|
||||
|
||||
// Convert metrics object to array
|
||||
const metricsArray = Object.values(metricsResponse.metrics);
|
||||
|
||||
return (
|
||||
<ComparisonView
|
||||
initialData={comparisonData}
|
||||
initialUrns={urns}
|
||||
metrics={metricsArray}
|
||||
selectedMetric={selectedMetric}
|
||||
/>
|
||||
);
|
||||
}
|
||||
277
nextjs-app/app/globals.css
Normal file
277
nextjs-app/app/globals.css
Normal file
@@ -0,0 +1,277 @@
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary: #6b7280;
|
||||
--success: #22c55e;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f9fafb;
|
||||
--bg-tertiary: #f3f4f6;
|
||||
|
||||
--text-primary: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
|
||||
--border-light: #e5e7eb;
|
||||
--border-medium: #d1d5db;
|
||||
--border-dark: #9ca3af;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
|
||||
--transition: 0.2s ease;
|
||||
}
|
||||
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* App Container */
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.noise-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
opacity: 0.03;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mt-1 { margin-top: 0.5rem; }
|
||||
.mt-2 { margin-top: 1rem; }
|
||||
.mt-3 { margin-top: 1.5rem; }
|
||||
.mt-4 { margin-top: 2rem; }
|
||||
|
||||
.mb-1 { margin-bottom: 0.5rem; }
|
||||
.mb-2 { margin-bottom: 1rem; }
|
||||
.mb-3 { margin-bottom: 1.5rem; }
|
||||
.mb-4 { margin-bottom: 2rem; }
|
||||
|
||||
/* Grid Layouts */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.grid-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-auto {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.grid-2,
|
||||
.grid-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.875rem;
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-medium);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 3px solid rgba(59, 130, 246, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--primary);
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar Styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-dark);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
51
nextjs-app/app/layout.tsx
Normal file
51
nextjs-app/app/layout.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Navigation } from '@/components/Navigation';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { ComparisonProvider } from '@/context/ComparisonProvider';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'SchoolCompare | Compare Primary School Performance',
|
||||
template: '%s | SchoolCompare',
|
||||
},
|
||||
description: 'Compare primary school KS2 performance across England',
|
||||
keywords: 'school comparison, KS2 results, primary school performance, England schools, SATs results',
|
||||
authors: [{ name: 'SchoolCompare' }],
|
||||
manifest: '/manifest.json',
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
title: 'SchoolCompare | Compare Primary School Performance',
|
||||
description: 'Compare primary school KS2 performance across England',
|
||||
url: 'https://schoolcompare.co.uk',
|
||||
siteName: 'SchoolCompare',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title: 'SchoolCompare | Compare Primary School Performance',
|
||||
description: 'Compare primary school KS2 performance across England',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<ComparisonProvider>
|
||||
<div className="app-container">
|
||||
<div className="noise-overlay" />
|
||||
<Navigation />
|
||||
<main className="main-content">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</ComparisonProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
53
nextjs-app/app/page.tsx
Normal file
53
nextjs-app/app/page.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Home Page (SSR)
|
||||
* Main landing page with school search and browsing
|
||||
*/
|
||||
|
||||
import { fetchSchools, fetchFilters } from '@/lib/api';
|
||||
import { HomeView } from '@/components/HomeView';
|
||||
|
||||
interface HomePageProps {
|
||||
searchParams: {
|
||||
search?: string;
|
||||
local_authority?: string;
|
||||
school_type?: string;
|
||||
page?: string;
|
||||
postcode?: string;
|
||||
radius?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Home',
|
||||
description: 'Search and compare primary school KS2 performance across England',
|
||||
};
|
||||
|
||||
// Force dynamic rendering (no static generation at build time)
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function HomePage({ searchParams }: HomePageProps) {
|
||||
// Parse search params
|
||||
const page = parseInt(searchParams.page || '1');
|
||||
const radius = searchParams.radius ? parseInt(searchParams.radius) : undefined;
|
||||
|
||||
// Fetch data on server
|
||||
const [schoolsData, filtersData] = await Promise.all([
|
||||
fetchSchools({
|
||||
search: searchParams.search,
|
||||
local_authority: searchParams.local_authority,
|
||||
school_type: searchParams.school_type,
|
||||
postcode: searchParams.postcode,
|
||||
radius,
|
||||
page,
|
||||
page_size: 50,
|
||||
}),
|
||||
fetchFilters(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<HomeView
|
||||
initialSchools={schoolsData}
|
||||
filters={filtersData.filters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
58
nextjs-app/app/rankings/page.tsx
Normal file
58
nextjs-app/app/rankings/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Rankings Page (SSR)
|
||||
* Display top-ranked schools by various metrics
|
||||
*/
|
||||
|
||||
import { fetchRankings, fetchFilters, fetchMetrics } from '@/lib/api';
|
||||
import { RankingsView } from '@/components/RankingsView';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
interface RankingsPageProps {
|
||||
searchParams: Promise<{
|
||||
metric?: string;
|
||||
local_authority?: string;
|
||||
year?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'School Rankings',
|
||||
description: 'Top-ranked primary schools by KS2 performance across England',
|
||||
keywords: 'school rankings, top schools, best schools, KS2 rankings, school league tables',
|
||||
};
|
||||
|
||||
// Force dynamic rendering
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function RankingsPage({ searchParams }: RankingsPageProps) {
|
||||
const { metric: metricParam, local_authority, year: yearParam } = await searchParams;
|
||||
|
||||
const metric = metricParam || 'rwm_expected_pct';
|
||||
const year = yearParam ? parseInt(yearParam) : undefined;
|
||||
|
||||
// Fetch rankings data
|
||||
const [rankingsResponse, filtersResponse, metricsResponse] = await Promise.all([
|
||||
fetchRankings({
|
||||
metric,
|
||||
local_authority,
|
||||
year,
|
||||
limit: 100,
|
||||
}),
|
||||
fetchFilters(),
|
||||
fetchMetrics(),
|
||||
]);
|
||||
|
||||
// Convert metrics object to array
|
||||
const metricsArray = Object.values(metricsResponse.metrics);
|
||||
|
||||
return (
|
||||
<RankingsView
|
||||
rankings={rankingsResponse.rankings}
|
||||
filters={filtersResponse.filters}
|
||||
metrics={metricsArray}
|
||||
selectedMetric={metric}
|
||||
selectedArea={local_authority}
|
||||
selectedYear={year}
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
nextjs-app/app/robots.ts
Normal file
19
nextjs-app/app/robots.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Robots.txt Configuration
|
||||
* Controls search engine crawling behavior
|
||||
*/
|
||||
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/api/', '/_next/'],
|
||||
},
|
||||
],
|
||||
sitemap: 'https://schoolcompare.co.uk/sitemap.xml',
|
||||
};
|
||||
}
|
||||
122
nextjs-app/app/school/[urn]/page.tsx
Normal file
122
nextjs-app/app/school/[urn]/page.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Individual School Page (SSR)
|
||||
* Dynamic route for school details with full SEO optimization
|
||||
*/
|
||||
|
||||
import { fetchSchoolDetails } from '@/lib/api';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { SchoolDetailView } from '@/components/SchoolDetailView';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
interface SchoolPageProps {
|
||||
params: Promise<{ urn: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: SchoolPageProps): Promise<Metadata> {
|
||||
const { urn: urnString } = await params;
|
||||
const urn = parseInt(urnString);
|
||||
|
||||
if (isNaN(urn) || urn < 100000 || urn > 999999) {
|
||||
return {
|
||||
title: 'School Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchSchoolDetails(urn);
|
||||
const { school_info } = data;
|
||||
|
||||
const title = `${school_info.school_name} | ${school_info.local_authority || 'England'}`;
|
||||
const description = `View KS2 performance data, results, and statistics for ${school_info.school_name}${school_info.local_authority ? ` in ${school_info.local_authority}` : ''}. Compare reading, writing, and maths results.`;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
keywords: `${school_info.school_name}, KS2 results, primary school, ${school_info.local_authority}, school performance, SATs results`,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: 'website',
|
||||
url: `https://schoolcompare.co.uk/school/${urn}`,
|
||||
siteName: 'SchoolCompare',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title,
|
||||
description,
|
||||
},
|
||||
alternates: {
|
||||
canonical: `https://schoolcompare.co.uk/school/${urn}`,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
title: 'School Not Found',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Force dynamic rendering
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function SchoolPage({ params }: SchoolPageProps) {
|
||||
const { urn: urnString } = await params;
|
||||
const urn = parseInt(urnString);
|
||||
|
||||
// Validate URN format
|
||||
if (isNaN(urn) || urn < 100000 || urn > 999999) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Fetch school data
|
||||
let data;
|
||||
try {
|
||||
data = await fetchSchoolDetails(urn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch school ${urn}:`, error);
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { school_info, yearly_data, absence_data } = data;
|
||||
|
||||
// Generate JSON-LD structured data for SEO
|
||||
const structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'EducationalOrganization',
|
||||
name: school_info.school_name,
|
||||
identifier: school_info.urn.toString(),
|
||||
...(school_info.address && {
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
streetAddress: school_info.address,
|
||||
addressLocality: school_info.local_authority || undefined,
|
||||
postalCode: school_info.postcode || undefined,
|
||||
addressCountry: 'GB',
|
||||
},
|
||||
}),
|
||||
...(school_info.latitude && school_info.longitude && {
|
||||
geo: {
|
||||
'@type': 'GeoCoordinates',
|
||||
latitude: school_info.latitude,
|
||||
longitude: school_info.longitude,
|
||||
},
|
||||
}),
|
||||
...(school_info.school_type && {
|
||||
additionalType: school_info.school_type,
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
||||
/>
|
||||
<SchoolDetailView
|
||||
schoolInfo={school_info}
|
||||
yearlyData={yearly_data}
|
||||
absenceData={absence_data}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
54
nextjs-app/app/sitemap.ts
Normal file
54
nextjs-app/app/sitemap.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Dynamic Sitemap Generation
|
||||
* Generates sitemap with all school pages and main routes
|
||||
*/
|
||||
|
||||
import { MetadataRoute } from 'next';
|
||||
import { fetchSchools } from '@/lib/api';
|
||||
|
||||
const BASE_URL = 'https://schoolcompare.co.uk';
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
// Static pages
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: BASE_URL,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily',
|
||||
priority: 1.0,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/compare`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/rankings`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
];
|
||||
|
||||
// Fetch all schools (in batches if necessary)
|
||||
try {
|
||||
const schoolsData = await fetchSchools({
|
||||
page: 1,
|
||||
page_size: 10000, // Fetch all schools
|
||||
});
|
||||
|
||||
const schoolPages: MetadataRoute.Sitemap = schoolsData.schools.map((school) => ({
|
||||
url: `${BASE_URL}/school/${school.urn}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
return [...staticPages, ...schoolPages];
|
||||
} catch (error) {
|
||||
console.error('Failed to generate sitemap:', error);
|
||||
// Return just static pages if school fetch fails
|
||||
return staticPages;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user