Baldr Template - Complete Routing System Guide
Framework: React Router v7 (SSR-enabled)
Last Updated: December 31, 2025
Version: 2.0.0
Table of Contents
- Overview
- Architecture Overview
- Database-Driven Pages
- Page Registry System
- Catchall Route System
- Sitemap Generation
- SEO & Meta Tags
- Module Pages
- Static Routes
- Adding New Pages
- Best Practices
Overview
Baldr Template uses a hybrid routing system that combines:
- Database-Driven Pages: Pages defined in MongoDB and served dynamically
- File-Based Routes: Static routes like
sitemap.xml,robots.txt - Component Registry: Maps page paths to React components
- Catchall Handler: Universal route that handles all database pages
This architecture allows you to:
- Manage content via CMS (Baldr-BO) without code deployment
- Create pages dynamically through admin interface
- Maintain SEO-friendly URLs with full control
- Generate sitemaps automatically with module subpages
Architecture Overview
Data Flow Diagram
User visits URL
↓
React Router matches route
↓
┌─────────────────────────────────────────┐
│ Static routes? │
│ (sitemap.xml, robots.txt) │
└─────────────────────────────────────────┘
↓ No
┌─────────────────────────────────────────┐
│ Catchall route ($) │
│ → Loader fetches page from DB │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Page data from backend │
│ (/api/page/path/:path) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ pageRegistry lookup │
│ → Maps path to React component │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Component renders with page data │
│ + SEO meta tags │
└─────────────────────────────────────────┘
System Components
┌──────────────────────────────────────────────────────────┐
│ Frontend Layer │
│ ┌────────────────────────────────────────────────────┐ │
│ │ app/config/pageRegistry.ts │ │
│ │ Maps: path → { component, moduleType? } │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ app/routes/$.tsx │ │
│ │ Catchall route with loader & meta │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ app/pages/*.page.tsx │ │
│ │ React components for each page │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
↕
┌──────────────────────────────────────────────────────────┐
│ API Layer │
│ ┌────────────────────────────────────────────────────┐ │
│ │ BaldrTs/src/controllers/page.controller.ts │ │
│ │ • getByPath(): Fetch page by URL path │ │
│ │ • getPublishedPages(): List with tableorder │ │
│ │ • getSitemapPages(): Generate sitemap data │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
↕
┌──────────────────────────────────────────────────────────┐
│ Database Layer │
│ ┌────────────────────────────────────────────────────┐ │
│ │ MongoDB: pages collection │ │
│ │ • path: "/produits", "/actualites", etc. │ │
│ │ • module_id: Link to module (optional) │ │
│ │ • seo: { title, description, keywords } │ │
│ │ • sitemap: { priority, changefreq, include } │ │
│ │ • active, indexed, publishedAt, expiresAt │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Database-Driven Pages
Page Schema (MongoDB)
interface IPage {
// Identity
_id: ObjectId;
title: string;
slug: string;
path: string; // URL path: "/produits", "/actualites"
// Module association
module_id?: ObjectId; // Links to Module collection
// Visibility
active: boolean; // Is page active?
indexed: boolean; // Should Google index this?
// Scheduling
scheduledAt?: Date; // Publish at specific time
expiresAt?: Date; // Auto-unpublish at this date
publishedAt?: Date;
// SEO Configuration
seo: {
title: string; // Browser tab title
description: string; // Meta description
keywords: string[]; // Meta keywords
ogTitle?: string; // OpenGraph title
ogDescription?: string; // OpenGraph description
ogImage?: string; // Social share image
canonicalUrl?: string; // Canonical URL
};
// Sitemap Configuration
sitemap: {
include: boolean; // Include in sitemap.xml?
priority: number; // 0.0 to 1.0
changefreq: string; // "daily", "weekly", "monthly"
};
// Translation support
translation: {
lang: string; // "fr", "en", etc.
ref_item?: ObjectId; // Links to original page
};
// Hierarchy
parentPage?: ObjectId;
childPages?: ObjectId[];
relatedPages?: ObjectId[];
}
Example Page Document
{
"_id": "676505a97b84cbc27a60e3f5",
"title": "Nos Produits",
"slug": "produits",
"path": "/produits",
"module_id": "69551b428891b3960912479e",
"active": true,
"indexed": true,
"publishedAt": "2024-12-20T10:00:00.000Z",
"seo": {
"title": "Découvrez Nos Produits | Tour du Jeu",
"description": "Parcourez notre catalogue de produits de qualité.",
"keywords": ["produits", "catalogue", "boutique"],
"ogImage": "/images/og-produits.jpg"
},
"sitemap": {
"include": true,
"priority": 0.8,
"changefreq": "weekly"
},
"translation": {
"lang": "fr",
"ref_item": null
}
}
Page Registry System
Purpose
The Page Registry (app/config/pageRegistry.ts) is the single source of truth that:
- Maps database page paths to React components
- Declares which pages display module content (for sitemap generation)
- Eliminates code duplication between routing and sitemap
Structure
/**
* Page configuration type
* - component: React component to render
* - moduleType: Optional module type if this page displays module content
*/
interface PageConfig {
component: ComponentType<any>;
moduleType?: string; // "products", "news", "vehicle", etc.
}
export const pageRegistry: Record<string, PageConfig> = {
"/": {
component: HomePage
},
"/en": {
component: HomePageEN
},
"/produits": {
component: ProduitsPage,
moduleType: "products" // ← This tells sitemap to generate product subpages
},
"/actualites": {
component: ActualitesPage,
moduleType: "news"
},
// ... more pages
};
Helper Functions
/**
* Get React component for a given path
*/
export function getPageComponent(path: string): ComponentType<any> | null {
const config = pageRegistry[path];
return config?.component || null;
}
/**
* Check if a page is registered
*/
export function hasPageComponent(path: string): boolean {
return path in pageRegistry;
}
/**
* Extract module-to-page mapping for sitemap generation
* Returns: { "/produits": "products", "/actualites": "news" }
*/
export function getModulePageMap(): Record<string, string> {
const modulePageMap: Record<string, string> = {};
for (const [path, config] of Object.entries(pageRegistry)) {
if (config.moduleType) {
modulePageMap[path] = config.moduleType;
}
}
return modulePageMap;
}
Why This Architecture?
Before (Duplicated Configuration):
// In pageRegistry.ts
const pageRegistry = {
"/produits": ProduitsPage,
"/actualites": ActualitesPage,
};
// In api.service.ts (DUPLICATE!)
const modulePageMap = {
"/produits": "products",
"/actualites": "news",
};
After (Single Source of Truth):
// In pageRegistry.ts (ONE PLACE!)
const pageRegistry = {
"/produits": { component: ProduitsPage, moduleType: "products" },
"/actualites": { component: ActualitesPage, moduleType: "news" },
};
// In api.service.ts (DERIVED!)
const modulePageMap = getModulePageMap(); // Auto-generated!
Benefits:
- ✅ No duplication
- ✅ Single place to add new pages
- ✅ Type-safe
- ✅ Automatic sitemap configuration
Catchall Route System
Route Definition (app/routes.ts)
import { type RouteConfig } from "@react-router/dev/routes";
export default [
// Static routes MUST come before catchall
{
path: "sitemap.xml",
file: "routes/sitemap[.]xml.tsx",
},
{
path: "robots.txt",
file: "routes/robots[.]txt.tsx",
},
// Catchall route - handles ALL database pages
// MUST be last to avoid overriding other routes
{
path: "*", // or path: "$" in newer syntax
file: "routes/$.tsx",
},
] satisfies RouteConfig;
Catchall Implementation (app/routes/$.tsx)
import { useLoaderData, type MetaFunction } from "react-router";
import { Suspense } from "react";
import { getPageComponent } from "../config/pageRegistry";
import { fetchPageByPath } from "../services/api.service";
import type { Page } from "../types/api.types";
/**
* Server-side loader
* Fetches page data from backend API
*/
export async function loader({ params }: LoaderFunctionArgs) {
const path = params["*"] || "/"; // Extract path from wildcard
try {
// Fetch page data from backend
const page = await fetchPageByPath(`/${path}`);
return { page };
} catch (error) {
// Page not found or error
throw new Response("Page Not Found", {
status: 404,
statusText: "The page you're looking for doesn't exist."
});
}
}
/**
* Generate meta tags for SEO
* Runs on server for SSR
*/
export const meta: MetaFunction = ({ data }) => {
if (!data?.page) {
return [{ title: "404 Not Found" }];
}
const { page } = data as { page: Page };
const seo = page.seo || {};
return [
{ title: seo.title || page.title },
{ name: "description", content: seo.description || "" },
{ name: "keywords", content: seo.keywords?.join(", ") || "" },
// OpenGraph tags
{ property: "og:title", content: seo.ogTitle || seo.title || page.title },
{ property: "og:description", content: seo.ogDescription || seo.description || "" },
{ property: "og:image", content: seo.ogImage || "" },
{ property: "og:type", content: "website" },
// Twitter Card
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: seo.ogTitle || seo.title || page.title },
{ name: "twitter:description", content: seo.ogDescription || seo.description || "" },
// Canonical URL
...(seo.canonicalUrl ? [{ tagName: "link", rel: "canonical", href: seo.canonicalUrl }] : []),
// Indexing directive
...(!page.indexed ? [{ name: "robots", content: "noindex, nofollow" }] : []),
];
};
/**
* Page component
* Looks up and renders the appropriate React component
*/
export default function CatchallPage() {
const { page } = useLoaderData<typeof loader>();
// Look up component in registry
const PageComponent = getPageComponent(page.path);
if (!PageComponent) {
return (
<div className="error-container">
<h1>Page Not Implemented</h1>
<p>The page "{page.path}" exists in the database but has no associated component.</p>
<p>Add it to <code>app/config/pageRegistry.ts</code></p>
</div>
);
}
// Render the page component with data
return (
<Suspense fallback={<div>Loading...</div>}>
<PageComponent page={page} />
</Suspense>
);
}
How It Works
- User visits
/produits - React Router matches catchall route (
*or$) - Loader runs on server:
- Extracts path from params:
"produits" - Calls API:
GET /api/page/path/produits - Returns page data
- Extracts path from params:
- Meta function generates SEO tags from page data
- Component renders:
- Looks up
pageRegistry["/produits"] - Finds
ProduitsPagecomponent - Renders with page data as props
- Looks up
Sitemap Generation
System Architecture
Frontend (baldr-template)
↓
pageRegistry.ts → getModulePageMap()
↓
{ "/produits": "products", "/actualites": "news" }
↓
routes/sitemap[.]xml.tsx → fetchSitemapPages(modulePageMap)
↓
Backend (BaldrTs)
↓
page.controller.ts → getSitemapPages()
↓
1. Fetch all published pages from DB
2. Use modulePageMap to find base paths
3. For each module type, fetch subpages:
- products → Product.find({ active: true })
- news → News.find({ active: true })
4. Generate XML with all URLs
Frontend: Sitemap Route (app/routes/sitemap[.]xml.tsx)
import type { LoaderFunctionArgs } from "react-router";
import { fetchSitemapPages } from "../services/api.service";
export async function loader({ request }: LoaderFunctionArgs) {
// Fetch sitemap data from backend
const sitemapData = await fetchSitemapPages();
// Generate XML
const xml = generateSitemapXML(sitemapData.items);
return new Response(xml, {
status: 200,
headers: {
"Content-Type": "application/xml",
"Cache-Control": "public, max-age=3600", // Cache for 1 hour
},
});
}
function generateSitemapXML(pages: SitemapPage[]): string {
const baseUrl = import.meta.env.VITE_BASE_URL || "https://tourdujeu.fr";
const urls = pages.map((page) => `
<url>
<loc>${escapeXml(baseUrl + page.path)}</loc>
<lastmod>${new Date(page.updatedAt).toISOString().split("T")[0]}</lastmod>
<changefreq>${page.sitemap.changefreq}</changefreq>
<priority>${page.sitemap.priority}</priority>
</url>`).join("");
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`;
}
Backend: Sitemap Controller
/**
* Get sitemap data (for sitemap.xml generation)
*
* Request body:
* {
* modulePageMap: {
* "/produits": "products",
* "/actualites": "news"
* }
* }
*/
getSitemapPages = async (req: Request, res: Response) => {
const { modulePageMap } = req.body;
// 1. Fetch all published pages
const pages = await Page.find({
active: true,
indexed: true,
"sitemap.include": true,
});
// 2. Build module-to-path map
const modulePathMap = new Map<string, string>();
if (modulePageMap) {
for (const [pagePath, moduleType] of Object.entries(modulePageMap)) {
const pageExists = pages.some((p) => p.path === pagePath);
if (pageExists) {
modulePathMap.set(moduleType, pagePath);
}
}
}
// 3. Generate subpages for each module
const generateSubpages = async (model: any, moduleType: string) => {
const basePath = modulePathMap.get(moduleType);
if (!basePath) return [];
const items = await model.find({
active: true,
"translation.ref_item": null, // Only original items
});
return items.map((item) => ({
path: `${basePath}/${item.slug}`,
updatedAt: item.updatedAt,
sitemap: {
priority: 0.6,
changefreq: "weekly",
},
}));
};
// 4. Fetch all subpages
const [products, news] = await Promise.all([
generateSubpages(Product, "products"),
generateSubpages(News, "news"),
]);
// 5. Combine and return
res.json({
items: [...pages, ...products, ...news],
total: pages.length + products.length + news.length,
});
};
Example Output
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- Main pages -->
<url>
<loc>https://tourdujeu.fr/</loc>
<lastmod>2025-12-31</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://tourdujeu.fr/produits</loc>
<lastmod>2025-12-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Product subpages (auto-generated) -->
<url>
<loc>https://tourdujeu.fr/produits/jeu-echecs-bois</loc>
<lastmod>2025-12-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://tourdujeu.fr/produits/monopoly-edition-classique</loc>
<lastmod>2025-12-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
</urlset>
SEO & Meta Tags
Page-Level SEO Configuration
Every page in the database has a seo object:
{
"seo": {
"title": "Page Title for Browser Tab",
"description": "Meta description for search results (150-160 chars)",
"keywords": ["keyword1", "keyword2", "keyword3"],
"ogTitle": "Social Media Share Title",
"ogDescription": "Description when shared on Facebook/LinkedIn",
"ogImage": "/images/social-share.jpg",
"canonicalUrl": "https://tourdujeu.fr/canonical-page"
}
}
Meta Tag Generation
The catchall route's meta function transforms page data into HTML meta tags:
export const meta: MetaFunction = ({ data }) => {
const { page } = data;
return [
// Basic SEO
{ title: page.seo.title },
{ name: "description", content: page.seo.description },
{ name: "keywords", content: page.seo.keywords.join(", ") },
// OpenGraph (Facebook, LinkedIn)
{ property: "og:title", content: page.seo.ogTitle },
{ property: "og:description", content: page.seo.ogDescription },
{ property: "og:image", content: page.seo.ogImage },
{ property: "og:url", content: `https://tourdujeu.fr${page.path}` },
{ property: "og:type", content: "website" },
// Twitter Card
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: page.seo.ogTitle },
{ name: "twitter:description", content: page.seo.ogDescription },
{ name: "twitter:image", content: page.seo.ogImage },
// Robots directive
{ name: "robots", content: page.indexed ? "index, follow" : "noindex, nofollow" },
// Canonical URL
{ tagName: "link", rel: "canonical", href: page.seo.canonicalUrl },
];
};
Rendered HTML
<head>
<title>Découvrez Nos Produits | Tour du Jeu</title>
<meta name="description" content="Parcourez notre catalogue de produits de qualité.">
<meta name="keywords" content="produits, catalogue, boutique">
<meta property="og:title" content="Nos Produits - Tour du Jeu">
<meta property="og:description" content="Découvrez notre sélection">
<meta property="og:image" content="https://tourdujeu.fr/images/og-produits.jpg">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://tourdujeu.fr/produits">
</head>
Module Pages
What are Module Pages?
Module pages display lists of content from a specific module (products, news, vehicles, etc.). They are:
- Defined in the database with a
module_idreference - Registered in
pageRegistrywith amoduleType - Used as base paths for sitemap subpage generation
Example: Products Page
Database:
{
"path": "/produits",
"title": "Nos Produits",
"module_id": "69551b428891b3960912479e" // ← Links to "products" module
}
Page Registry:
"/produits": {
component: ProduitsPage,
moduleType: "products" // ← Tells sitemap to generate product subpages
}
Page Component (app/pages/Produits.page.tsx):
import { useEffect, useState } from "react";
import { useModules } from "../context/ModuleContext";
import { fetchPages } from "../services/api.service";
export default function ProduitsPage({ page }: { page: Page }) {
const { modules } = useModules();
const [products, setProducts] = useState([]);
// Find the products module
const productsModule = modules.find((m) => m.type === "products");
useEffect(() => {
if (productsModule) {
// Fetch pages ordered by tableorder
fetchPages(productsModule._id, ["pinned:desc"]).then(setProducts);
}
}, [productsModule]);
return (
<div className="products-page">
<h1>{page.title}</h1>
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product._id} product={product} />
))}
</div>
</div>
);
}
Sitemap Subpage Generation
When sitemap is generated:
- Frontend sends:
{ "/produits": "products" } - Backend receives this and:
- Finds base path:
/produits - Queries Product model:
Product.find({ active: true }) - Generates URLs:
/produits/jeu-echecs,/produits/monopoly
- Finds base path:
- Sitemap includes:
<url><loc>/produits</loc></url> <!-- Main page -->
<url><loc>/produits/jeu-echecs</loc></url> <!-- Subpage 1 -->
<url><loc>/produits/monopoly</loc></url> <!-- Subpage 2 -->
Static Routes
Static Route Definition
Static routes like sitemap.xml and robots.txt are defined before the catchall:
// app/routes.ts
export default [
// Static routes (MUST come first)
{
path: "sitemap.xml",
file: "routes/sitemap[.]xml.tsx",
},
{
path: "robots.txt",
file: "routes/robots[.]txt.tsx",
},
// Catchall (MUST be last)
{
path: "*",
file: "routes/$.tsx",
},
] satisfies RouteConfig;
Example: robots.txt
// app/routes/robots[.]txt.tsx
import type { LoaderFunctionArgs } from "react-router";
export async function loader({ request }: LoaderFunctionArgs) {
const baseUrl = new URL(request.url).origin;
const robotsTxt = `# Baldr Template Robots.txt
User-agent: *
Allow: /
# Sitemap
Sitemap: ${baseUrl}/sitemap.xml
# Disallow admin paths (if any)
Disallow: /admin/
Disallow: /api/
`;
return new Response(robotsTxt, {
status: 200,
headers: {
"Content-Type": "text/plain",
"Cache-Control": "public, max-age=86400", // 24 hours
},
});
}
Adding New Pages
Step-by-Step Guide
1. Create Page in Database (via Baldr-BO)
Navigate to: SEO → Pages → New Page
Fill in:
- Title: "Nos Actualités"
- Slug: "actualites"
- Path: "/actualites"
- Module: Select "News" module (if displaying news)
- Active: ✓ Yes
- Indexed: ✓ Yes (for SEO)
- Sitemap Include: ✓ Yes
- Sitemap Priority: 0.7
- Sitemap Change Frequency: daily
SEO Configuration:
- Title: "Actualités | Tour du Jeu"
- Description: "Toutes nos actualités et nouveautés"
- Keywords: actualités, news, nouveautés
- OG Image: Upload social share image
2. Create React Component
Create app/pages/Actualites.page.tsx:
import { useEffect, useState } from "react";
import { useModules } from "../context/ModuleContext";
import { fetchPages } from "../services/api.service";
import type { Page } from "../types/api.types";
export default function ActualitesPage({ page }: { page: Page }) {
const { modules } = useModules();
const [news, setNews] = useState([]);
const newsModule = modules.find((m) => m.type === "news");
useEffect(() => {
if (newsModule) {
fetchPages(newsModule._id, ["pinned:desc"]).then(setNews);
}
}, [newsModule]);
return (
<div className="actualites-page">
<h1>{page.title}</h1>
<p>{page.seo?.description}</p>
<div className="news-list">
{news.map((item) => (
<article key={item._id} className="news-card">
<h2>{item.title}</h2>
<time>{new Date(item.publishedAt).toLocaleDateString("fr-FR")}</time>
<p>{item.subtitle}</p>
<a href={`/actualites/${item.slug}`}>Lire la suite →</a>
</article>
))}
</div>
</div>
);
}
3. Register in Page Registry
Update app/config/pageRegistry.ts:
import ActualitesPage from "../pages/Actualites.page";
export const pageRegistry: Record<string, PageConfig> = {
"/": { component: HomePage },
"/produits": { component: ProduitsPage, moduleType: "products" },
// Add new page here
"/actualites": {
component: ActualitesPage,
moduleType: "news" // ← Enables news subpages in sitemap
},
};
4. Test the Page
-
Start servers:
# Terminal 1: Backend
cd BaldrTs && npm run dev
# Terminal 2: Frontend
cd baldr-template && npm run dev -
Visit:
http://localhost:5174/actualites -
Check sitemap:
http://localhost:5174/sitemap.xml- Should include
/actualitesmain page - Should include news subpages:
/actualites/news-slug-1, etc.
- Should include
-
Verify SEO:
- View page source
- Check
<title>,<meta>, and OpenGraph tags
Best Practices
1. Page Registry Organization
// Group by feature/section
export const pageRegistry: Record<string, PageConfig> = {
// Home & Static Pages
"/": { component: HomePage },
"/en": { component: HomePageEN },
"/a-propos": { component: AboutPage },
"/contact": { component: ContactPage },
// Module Pages (with subpage generation)
"/produits": { component: ProduitsPage, moduleType: "products" },
"/actualites": { component: ActualitesPage, moduleType: "news" },
"/vehicules": { component: VehiculesPage, moduleType: "vehicle" },
// Dynamic Content Pages
"/evenements": { component: EventsPage, moduleType: "event" },
"/galeries": { component: GalleriesPage, moduleType: "gallery" },
};
2. SEO Optimization
Always provide:
- Unique
title(50-60 characters) - Compelling
description(150-160 characters) - Relevant
keywords(5-10 words) - High-quality
ogImage(1200x630px recommended)
Use structured data:
// In page component
export function generateStructuredData(page: Page) {
return {
"@context": "https://schema.org",
"@type": "WebPage",
"name": page.title,
"description": page.seo.description,
"url": `https://tourdujeu.fr${page.path}`,
};
}
3. Performance
Lazy load page components:
const ProduitsPage = lazy(() => import("../pages/Produits.page"));
Use Suspense in catchall:
<Suspense fallback={<PageSkeleton />}>
<PageComponent page={page} />
</Suspense>
4. Error Handling
Handle missing pages gracefully:
export async function loader({ params }: LoaderFunctionArgs) {
try {
const page = await fetchPageByPath(`/${params["*"]}`);
return { page };
} catch (error) {
if (error.statusCode === 404) {
throw new Response("Page Not Found", { status: 404 });
}
throw error;
}
}
Provide helpful error boundary:
export function ErrorBoundary() {
const error = useRouteError();
return (
<div className="error-page">
<h1>Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link to="/">← Back to Home</Link>
</div>
);
}
5. Sitemap Best Practices
Priority guidelines:
- Homepage: 1.0
- Main sections: 0.8
- Secondary pages: 0.6
- Detail pages: 0.4-0.6
Change frequency:
- Homepage/News:
daily - Products/Services:
weekly - Static pages:
monthly - Archive:
yearly
6. Module Type Naming
Use consistent naming:
- Database:
"products"(plural, lowercase) - Page registry:
moduleType: "products"(matches database) - Model name:
Product(singular, PascalCase)
Architecture Decisions & Rationale
Why Database-Driven Pages?
Benefits:
- ✅ Non-technical users can create pages via CMS
- ✅ No code deployment needed for content changes
- ✅ SEO configuration centralized in database
- ✅ Scheduled publishing (publish/unpublish automatically)
- ✅ Multi-language support via translation system
- ✅ Page hierarchy and relationships
Trade-offs:
- ⚠️ Requires database query for each page load (mitigated by caching)
- ⚠️ Must maintain component registry in code
Why Page Registry?
Alternatives considered:
- File-system routing: Too rigid, requires developer for each page
- Dynamic imports by convention: Fragile, hard to debug
- Full CMS rendering: Too complex, loses React benefits
Chosen solution: Hybrid Registry
- Developers control components (type-safe, tested)
- Content team controls data (flexible, immediate updates)
- Best of both worlds
Why POST for Sitemap Endpoint?
Why not GET?
modulePageMapcan be large- GET has URL length limits
- POST body is cleaner for complex data
Why send from frontend?
- Frontend knows its own structure
- Backend doesn't need to hardcode module-page relationships
- Decoupled architecture
Troubleshooting
Page not rendering
Problem: Page exists in DB but shows "Page Not Implemented"
Solution: Add to page registry:
"/your-path": { component: YourPageComponent }
Subpages missing from sitemap
Problem: Main page in sitemap but no subpages
Solution: Add moduleType to registry:
"/your-path": {
component: YourPageComponent,
moduleType: "your-module-type" // ← Add this
}
SEO tags not updating
Problem: Changed SEO in DB but meta tags unchanged
Solution:
- Clear browser cache
- Restart frontend server (meta runs at build time)
- Verify page data in loader
404 on valid page
Problem: Page exists but returns 404
Checklist:
- Page
active: truein database? - Page
indexed: true? -
publishedAtis in the past? -
expiresAtis null or in future? - Backend server running?
- Correct API URL in
.env?
Summary
The Baldr Template routing system:
- Database stores page definitions with SEO & scheduling
- Page Registry maps paths to React components (single source of truth)
- Catchall Route handles all database pages dynamically
- Sitemap Generator uses registry to create SEO-friendly XML
- Module Pages display content with automatic subpage generation
Key files:
app/config/pageRegistry.ts- Component & module mappingapp/routes/$.tsx- Universal page handlerapp/routes/sitemap[.]xml.tsx- Sitemap generatorapp/services/api.service.ts- Backend communicationBaldrTs/src/controllers/page.controller.ts- Page API
To add a page:
- Create in Baldr-BO (SEO → Pages)
- Create React component in
app/pages/ - Register in
app/config/pageRegistry.ts - Test and verify sitemap
Architecture Benefits:
- ✅ Content team autonomy
- ✅ Developer control of components
- ✅ Automatic SEO & sitemap generation
- ✅ Type-safe and maintainable
- ✅ Server-side rendering ready
Last Updated: December 31, 2025
Next Review: Quarterly or when adding major routing features