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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 `<p>`, `<ul>`, `<ol>` 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.
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion frontend/.husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
cd frontend
pnpm format
pnpm build
pnpm build
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion frontend/src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down Expand Up @@ -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: () => <BaseModelsPage />,
};
},
},
{
path: APPLICATION_ROUTES.BASE_MODEL_DETAILS,
lazy: async () => {
const { BaseModelDetailPage } = await import(
"@/app/routes/base-models/base-model-detail"
);
return {
Component: () => <BaseModelDetailPage />,
};
},
},

/**
* Base Models routes ends.
*/
{
path: APPLICATION_ROUTES.MODEL_DETAILS,
lazy: async () => {
Expand Down Expand Up @@ -441,5 +472,9 @@ const router = createBrowserRouter([
]);

export const AppRouter = () => {
return <RouterProvider router={router} />;
return (
<NuqsAdapter>
<RouterProvider router={router} />
</NuqsAdapter>
);
};
Loading