Aller au contenu principal

Baldr Template - Complete Routing System Guide

Framework: React Router v7 (SSR-enabled)
Last Updated: December 31, 2025
Version: 2.0.0


Table of Contents

  1. Overview
  2. Architecture Overview
  3. Database-Driven Pages
  4. Page Registry System
  5. Catchall Route System
  6. Sitemap Generation
  7. SEO & Meta Tags
  8. Module Pages
  9. Static Routes
  10. Adding New Pages
  11. Best Practices

Overview

Baldr Template uses a hybrid routing system that combines:

  1. Database-Driven Pages: Pages defined in MongoDB and served dynamically
  2. File-Based Routes: Static routes like sitemap.xml, robots.txt
  3. Component Registry: Maps page paths to React components
  4. 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:

  1. Maps database page paths to React components
  2. Declares which pages display module content (for sitemap generation)
  3. 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

  1. User visits /produits
  2. React Router matches catchall route (* or $)
  3. Loader runs on server:
    • Extracts path from params: "produits"
    • Calls API: GET /api/page/path/produits
    • Returns page data
  4. Meta function generates SEO tags from page data
  5. Component renders:
    • Looks up pageRegistry["/produits"]
    • Finds ProduitsPage component
    • Renders with page data as props

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:

  1. Defined in the database with a module_id reference
  2. Registered in pageRegistry with a moduleType
  3. 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:

  1. Frontend sends: { "/produits": "products" }
  2. Backend receives this and:
    • Finds base path: /produits
    • Queries Product model: Product.find({ active: true })
    • Generates URLs: /produits/jeu-echecs, /produits/monopoly
  3. 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

  1. Start servers:

    # Terminal 1: Backend
    cd BaldrTs && npm run dev

    # Terminal 2: Frontend
    cd baldr-template && npm run dev
  2. Visit: http://localhost:5174/actualites

  3. Check sitemap: http://localhost:5174/sitemap.xml

    • Should include /actualites main page
    • Should include news subpages: /actualites/news-slug-1, etc.
  4. 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:

  1. File-system routing: Too rigid, requires developer for each page
  2. Dynamic imports by convention: Fragile, hard to debug
  3. 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?

  • modulePageMap can 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:

  1. Clear browser cache
  2. Restart frontend server (meta runs at build time)
  3. Verify page data in loader

404 on valid page

Problem: Page exists but returns 404

Checklist:

  • Page active: true in database?
  • Page indexed: true?
  • publishedAt is in the past?
  • expiresAt is null or in future?
  • Backend server running?
  • Correct API URL in .env?

Summary

The Baldr Template routing system:

  1. Database stores page definitions with SEO & scheduling
  2. Page Registry maps paths to React components (single source of truth)
  3. Catchall Route handles all database pages dynamically
  4. Sitemap Generator uses registry to create SEO-friendly XML
  5. Module Pages display content with automatic subpage generation

Key files:

  • app/config/pageRegistry.ts - Component & module mapping
  • app/routes/$.tsx - Universal page handler
  • app/routes/sitemap[.]xml.tsx - Sitemap generator
  • app/services/api.service.ts - Backend communication
  • BaldrTs/src/controllers/page.controller.ts - Page API

To add a page:

  1. Create in Baldr-BO (SEO → Pages)
  2. Create React component in app/pages/
  3. Register in app/config/pageRegistry.ts
  4. 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