diff --git a/docs/decisions/frontend/architecture/adr-choose-markdown-library/adr1.md b/docs/decisions/frontend/architecture/adr-choose-markdown-library/adr1.md
new file mode 100644
index 000000000..3ef5988fd
--- /dev/null
+++ b/docs/decisions/frontend/architecture/adr-choose-markdown-library/adr1.md
@@ -0,0 +1,46 @@
+# Architecture Decision Record 1: Use react-markdown with remark-gfm for Rendering Markdown Content
+
+Date: 05/03/2026
+
+# Context
+
+The base model detail page displays long-form content (overview, use cases, performance, limitations) that was previously rendered using manual paragraph splitting and hardcoded HTML structures. As content grows in complexity — with bold text, inline code, lists, headings, and links — maintaining this as plain strings with custom rendering logic becomes difficult and error-prone.
+
+We need a solution to render rich, structured text from markdown strings so that content authors can express formatting naturally, while the UI consistently applies the project's design system.
+
+## Decision Drivers
+
+- Content flexibility: authors should be able to use headings, bold, lists, code, and links without code changes.
+- Consistency: rendered markdown must match the application's existing design system (colors, typography, spacing).
+- Minimal bundle impact: the chosen library should be lightweight and avoid unnecessary overhead.
+- Existing ecosystem: leverage libraries already present in the project wherever possible.
+- Security: HTML should be sanitised by default to prevent XSS from user-supplied content.
+
+## Considered Options
+
+- **[react-markdown](https://github.com/remarkjs/react-markdown) + [remark-gfm](https://github.com/remarkjs/remark-gfm)** — A lightweight React component that converts markdown to React elements via the unified/remark ecosystem. `remark-gfm` adds GitHub Flavored Markdown support (tables, strikethrough, task lists, autolinks). Does **not** use `dangerouslySetInnerHTML`; it builds a React virtual DOM tree. Already installed as project dependencies.
+- **[markdown-to-jsx](https://www.npmjs.com/package/markdown-to-jsx)** — A single-package alternative that compiles markdown to JSX. Slightly smaller bundle size, but lacks the plugin ecosystem of remark and does not support GFM features without extra work.
+
+- **Custom rendering logic** — Continue splitting strings on `\n\n` and mapping to `
`, `
`, `` elements manually. Does not scale as content grows in richness.
+
+# Decision
+
+We will use **react-markdown** (v9) with the **remark-gfm** plugin to render all long-form content in the frontend starting with base model detail page.
+
+Key implementation details:
+
+1. **Data model simplification**: The separate `overview`, `useCases`, `performance`, and `limitations` fields on `TBaseModelDetail` are consolidated into a single `markdownContent: string` field containing full markdown.
+2. **Styling**: The Tailwind CSS `@tailwindcss/typography` plugin's `prose` class is used as the base, with a scoped `.model-detail-prose` CSS class that overrides defaults to match the application's design tokens (colors, font sizes, spacing).
+3. **Banner isolation**: The existing banner component's `.prose *` white-text override is scoped to `.prose:not(.model-detail-prose)` so the two contexts do not conflict.
+
+# Status
+
+Accepted.
+
+# Consequences
+
+- **Positive**: Content is now authored in standard markdown, making it easier to update and maintain. Markdown supports headings, bold, italic, lists, inline code, links, and tables out of the box.
+- **Positive**: No new dependencies added — `react-markdown`, `remark-gfm`, and `@tailwindcss/typography` were already in `package.json`.
+- **Positive**: Safe by default — `react-markdown` does not use `dangerouslySetInnerHTML` and builds React elements directly.
+- **Trade-off**: Content structure is now implicit in the markdown string rather than explicit in the TypeScript type. If specific sections need to be programmatically accessed separately (e.g., extracting just the overview), parsing the markdown would be required.
+- **Trade-off**: Custom `.model-detail-prose` CSS styles need to be maintained alongside the design system. If design tokens change, these styles must be updated accordingly.
diff --git a/docs/decisions/frontend/architecture/adr-choose-url-state-library/adr1.md b/docs/decisions/frontend/architecture/adr-choose-url-state-library/adr1.md
new file mode 100644
index 000000000..d6a7ab4b3
--- /dev/null
+++ b/docs/decisions/frontend/architecture/adr-choose-url-state-library/adr1.md
@@ -0,0 +1,40 @@
+# Architecture Decision Record 1: Use nuqs for URL-based UI State Management
+
+Date: 02/03/2026
+
+# Context
+
+The frontend currently has multiple pages with search and filter controls that are reflected in URL query parameters. Historically, some pages managed this with ad-hoc utilities and manual synchronization between component state and `useSearchParams`, which increased complexity and inconsistency.
+
+We have validated `nuqs` in the start mapping flow and found it to be a performant and ergonomic approach for query-string state handling. As more pages require URL-based state, we need a consistent, typed pattern across the frontend.
+
+## Decision Drivers
+
+- Consistent URL-state behavior across routes with search and filters.
+- Better type safety and parsing for query params than manual string handling.
+- Simpler implementation and maintenance compared to custom synchronization utilities.
+- Good performance for frequent UI state updates tied to query parameters.
+- Better developer experience and readability for future feature work.
+
+## Considered Options
+
+- Continue using `react-router-dom` `useSearchParams` with custom helper utilities.
+- Use [`nuqs`](https://nuqs.dev/) as the standard query-state library.
+- Keep filter/search state only in component/global state and avoid URL synchronization.
+
+# Decision
+
+We will standardize on `nuqs` for managing URL-based state in frontend routes that need query-parameter-backed UI state (for example: search text, filters, sorting, map/list toggles, pagination, and similar controls).
+
+`react-router-dom` `useSearchParams` may still be used for simple one-off cases, but all new or refactored complex query-state flows should use `nuqs` as the default pattern.
+
+# Status
+
+Accepted.
+
+# Consequences
+
+- Query-state logic becomes more consistent and easier to reason about across pages.
+- We reduce repeated boilerplate for parsing/serializing query parameters.
+- Existing pages that use custom URL-state utilities may need incremental migration to align with this decision.
+- Team members should follow the `nuqs` pattern used in the start mapping implementation as the reference approach.
diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit
index 0d6108f45..5ecd813c5 100755
--- a/frontend/.husky/pre-commit
+++ b/frontend/.husky/pre-commit
@@ -1,3 +1,3 @@
cd frontend
pnpm format
-pnpm build
+pnpm build
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index f01b4cb4c..c79fae168 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -32,6 +32,7 @@
"framer-motion": "^12.19.1",
"geojson": "^0.5.0",
"maplibre-gl": "^5.3.1",
+ "nuqs": "^2.8.8",
"pmtiles": "^4.3.0",
"react": "19.1.0",
"react-confetti-explosion": "^3.0.3",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 22a863e89..941752d8e 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -63,6 +63,9 @@ importers:
maplibre-gl:
specifier: ^5.3.1
version: 5.3.1
+ nuqs:
+ specifier: ^2.8.8
+ version: 2.8.8(react-router-dom@6.26.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@6.26.2(react@19.1.0))(react@19.1.0)
pmtiles:
specifier: ^4.3.0
version: 4.3.0
@@ -933,6 +936,9 @@ packages:
resolution: {integrity: sha512-fB9+bPHLg5zVwPbBKEqY3ghyttkJq9RuUzFMTZKweKrNKKDMUACtI8DlMYUqNwpdZMJhf7a0xeak6vFVBSxcbQ==}
engines: {node: '>=14.17.0'}
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
'@tailwindcss/typography@0.5.15':
resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==}
peerDependencies:
@@ -2578,6 +2584,27 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
+ nuqs@2.8.8:
+ resolution: {integrity: sha512-LF5sw9nWpHyPWzMMu9oho3r9C5DvkpmBIg4LQN78sexIzGaeRx8DWr0uy3YiFx5i2QGZN1Qqcb+OAtEVRa2bnA==}
+ peerDependencies:
+ '@remix-run/react': '>=2'
+ '@tanstack/react-router': ^1
+ next: '>=14.2.0'
+ react: '>=18.2.0 || ^19.0.0-0'
+ react-router: ^5 || ^6 || ^7
+ react-router-dom: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ '@remix-run/react':
+ optional: true
+ '@tanstack/react-router':
+ optional: true
+ next:
+ optional: true
+ react-router:
+ optional: true
+ react-router-dom:
+ optional: true
+
nwsapi@2.2.16:
resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
@@ -4161,6 +4188,8 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
+ '@standard-schema/spec@1.0.0': {}
+
'@tailwindcss/typography@0.5.15(tailwindcss@3.4.13)':
dependencies:
lodash.castarray: 4.4.0
@@ -6298,6 +6327,14 @@ snapshots:
normalize-range@0.1.2: {}
+ nuqs@2.8.8(react-router-dom@6.26.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@6.26.2(react@19.1.0))(react@19.1.0):
+ dependencies:
+ '@standard-schema/spec': 1.0.0
+ react: 19.1.0
+ optionalDependencies:
+ react-router: 6.26.2(react@19.1.0)
+ react-router-dom: 6.26.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+
nwsapi@2.2.16: {}
object-assign@4.1.1: {}
diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx
index 3f544b69c..17471ab8f 100644
--- a/frontend/src/app/router.tsx
+++ b/frontend/src/app/router.tsx
@@ -12,6 +12,7 @@ import {
createBrowserRouter,
} from "react-router-dom";
import { ModelsProvider } from "@/app/providers/models-provider";
+import { NuqsAdapter } from "nuqs/adapters/react-router/v6";
const router = createBrowserRouter([
{
@@ -54,6 +55,36 @@ const router = createBrowserRouter([
/**
* Models details, list and feedbacks route starts.
*/
+
+ /**
+ * Base Models routes.
+ */
+ {
+ path: APPLICATION_ROUTES.BASE_MODELS_HOME,
+ lazy: async () => {
+ const { BaseModelsPage } = await import(
+ "@/app/routes/base-models/base-models-list"
+ );
+ return {
+ Component: () => ,
+ };
+ },
+ },
+ {
+ path: APPLICATION_ROUTES.BASE_MODEL_DETAILS,
+ lazy: async () => {
+ const { BaseModelDetailPage } = await import(
+ "@/app/routes/base-models/base-model-detail"
+ );
+ return {
+ Component: () => ,
+ };
+ },
+ },
+
+ /**
+ * Base Models routes ends.
+ */
{
path: APPLICATION_ROUTES.MODEL_DETAILS,
lazy: async () => {
@@ -441,5 +472,9 @@ const router = createBrowserRouter([
]);
export const AppRouter = () => {
- return ;
+ return (
+
+
+
+ );
};
diff --git a/frontend/src/app/routes/base-models/base-model-detail.tsx b/frontend/src/app/routes/base-models/base-model-detail.tsx
new file mode 100644
index 000000000..a26c2272e
--- /dev/null
+++ b/frontend/src/app/routes/base-models/base-model-detail.tsx
@@ -0,0 +1,323 @@
+import { Head } from "@/components/seo";
+import MarkdownViewer from "@/components/shared/markdown-render";
+import { BackButton, ButtonWithIcon } from "@/components/ui/button";
+import { ChevronDownIcon, InfoIcon } from "@/components/ui/icons";
+import { DownloadIconNew } from "@/components/ui/icons/download-icon";
+import { ToolTip } from "@/components/ui/tooltip";
+import { APPLICATION_ROUTES } from "@/constants";
+import { ButtonVariant } from "@/enums";
+
+import AccuracyDisplay from "@/features/models/components/accuracy-display";
+import {
+ BASE_MODELS_DETAIL_DATA,
+ TBaseModelDetail,
+ TBaseModelVariant,
+} from "@/utils/base-model-data";
+import { useEffect, useMemo, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+type TInfoRowConfig = {
+ label: string;
+ value: string;
+ tooltip?: string;
+};
+
+type TMetadataItemProps = {
+ label: string;
+ value: React.ReactNode;
+ tooltip?: string;
+};
+
+/**
+ * Collapsible section component for the right sidebar.
+ */
+const CollapsibleSection = ({
+ title,
+ children,
+ defaultOpen = true,
+}: {
+ title: string;
+ children: React.ReactNode;
+ defaultOpen?: boolean;
+}) => {
+ const [isOpen, setIsOpen] = useState(defaultOpen);
+
+ return (
+
+ );
+};
+
+export default MarkdownViewer;
diff --git a/frontend/src/components/ui/icons/download-icon.tsx b/frontend/src/components/ui/icons/download-icon.tsx
index a4cc9407a..d9417f5be 100644
--- a/frontend/src/components/ui/icons/download-icon.tsx
+++ b/frontend/src/components/ui/icons/download-icon.tsx
@@ -14,3 +14,19 @@ export const DownloadIcon: React.FC = (props) => (
/>
);
+
+export const DownloadIconNew = (props: IconProps) => (
+
+);
diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts
index c29be2e37..ac86c13c1 100644
--- a/frontend/src/constants/routes.ts
+++ b/frontend/src/constants/routes.ts
@@ -27,6 +27,11 @@ export const APPLICATION_ROUTES = {
MODEL_DETAILS: `${MODELS_BASE}/:id`,
MODEL_FEEDBACKS: `${MODELS_BASE}/:id/feedbacks`,
+ // base-model start
+ BASE_MODELS_HOME: "/base-models",
+ BASE_MODEL_DETAILS: "/base-models/:id",
+ // base-model end
+
// Model routes start
CREATE_NEW_MODEL: `${MODELS_ROUTES.CREATE_MODEL_BASE}/${MODELS_ROUTES.DETAILS}`,
diff --git a/frontend/src/constants/ui-contents/shared-content.ts b/frontend/src/constants/ui-contents/shared-content.ts
index 3be718a5a..073c64e57 100644
--- a/frontend/src/constants/ui-contents/shared-content.ts
+++ b/frontend/src/constants/ui-contents/shared-content.ts
@@ -138,6 +138,13 @@ export const SHARED_CONTENT: TSharedContent = {
paragraph:
"fAIr is a collaborative project. We welcome all types of experience to join our community on HOTOSM Slack. There is always a room for AI/ML for earth observation expertise, community engagement enthusiastic, academic researcher or student looking for an academic challenge around social impact.",
},
+ baseModelCTA: {
+ title: "Contribute Your Base Model",
+ description:
+ "Contribute a base model to fAIr and help teams turn imagery into actionable map data, faster and more reliably.",
+ ctaButton: "Contribute",
+ ctaLink: "/base-models",
+ },
},
pageNotFound: {
messages: {
@@ -152,6 +159,107 @@ export const SHARED_CONTENT: TSharedContent = {
pageNotFound: "go to homepage",
},
},
+ baseModelsPage: {
+ pageHeadingTitle: "Base Models",
+ pageHeadingDescription:
+ " Each model is trained using one of the training datasets. Published models can be used to find mappable features in imagery that is similar to the training areas that dataset comes from.",
+ pageHeadingButtonText: "Contribute model",
+ contributeModelDialog: {
+ label: "Model Contribution Journey",
+ intro:
+ "Model contribution into fAIr is handled in GITHUB /fAIr-models repository. Here are high level explanation for the contribution four steps and detailed documentation is available when you go to GITHUB",
+ github: {
+ title: "Fair Model github",
+ href: "https://github.com/hotosm/fAIr-models",
+ buttonLabel: "GO TO GITHUB",
+ },
+ steps: [
+ {
+ title: "Complete Prerequisites",
+ description:
+ "Before opening a Pull Request, verify your model meets the technical and legal standards.",
+ sections: [
+ {
+ title: "Define Licenses",
+ description:
+ "AI models require three distinct licenses. You must select one for each category:",
+ listType: "unordered",
+ items: [
+ "Code License: (e.g., Apache 2.0, MIT, or GPLv3)",
+ "Weights License: (e.g., Apache 2.0, CC BY 4.0, or Custom)",
+ "Data License: (e.g., CC BY, CC BY-NC, or Custom Terms)",
+ ],
+ note: "Note: This will be automatically validated if your selections are HOT-compliant to prevent future rejection.",
+ },
+ {
+ title: "Verify Model Endpoints",
+ description:
+ "Ensure your model code includes the four mandatory API endpoints:",
+ listType: "ordered",
+ items: [
+ "Training: For model fine-tuning.",
+ "Inference: For generating predictions.",
+ "Preprocessing: For imagery preparation.",
+ "Postprocessing: For cleaning and formatting results.",
+ ],
+ },
+ {
+ title: "Define Input/Output Shape",
+ description:
+ "Clearly describe the data formats your model handles.",
+ listType: "unordered",
+ items: [
+ "Input Example: Image RGB (tiles) + GeoJSON (labels)",
+ "Output Example: GeoJSON (detections) or Mask raster (segmentation)",
+ ],
+ },
+ {
+ title: "Select Task Category",
+ description: "Choose one of the currently supported tasks:",
+ listType: "unordered",
+ items: [
+ "Semantic Segmentation",
+ "Instance Segmentation",
+ "Object Detection (Selected for this session)",
+ ],
+ },
+ ],
+ },
+ {
+ title: "Review Guidelines",
+ description:
+ "To align with our community standards, you must read and acknowledge the contribution rules.",
+ },
+ {
+ title: "Submit and Track PR",
+ description:
+ "After reviewing the guidelines and finished the prerequisites, you can now open a PR.",
+ },
+ {
+ title: "Approval & Deployment",
+ description:
+ "Your contribution enters the final review stage by the fAIr maintainers.",
+ statuses: [
+ {
+ variant: "pending",
+ label: "🟡 Pending",
+ description: "Under review by maintainers or CI is running.",
+ },
+ {
+ variant: "changes",
+ label: "🔴 Needs Changes",
+ description: "Feedback has been provided; updates are required.",
+ },
+ {
+ variant: "approved",
+ label: "🟢 Approved",
+ description: "PR is merged! Your model is now a fAIr base model.",
+ },
+ ],
+ },
+ ],
+ },
+ },
protectedPage: {
ctaButton: "login",
messageParagraph: "To access this page you have to login.",
diff --git a/frontend/src/features/base-models/components/base-model-card.tsx b/frontend/src/features/base-models/components/base-model-card.tsx
new file mode 100644
index 000000000..de365bdac
--- /dev/null
+++ b/frontend/src/features/base-models/components/base-model-card.tsx
@@ -0,0 +1,48 @@
+import { Link } from "@/components/ui/link";
+import { APPLICATION_ROUTES } from "@/constants";
+import { TBaseModel } from "@/utils/base-model-data";
+import { roundNumber } from "@/utils/number-utils";
+
+type BaseModelCardProps = {
+ model: TBaseModel;
+};
+
+const BaseModelCard: React.FC = ({ model }) => {
+ return (
+
+ {/* Model Name */}
+