diff --git a/docs/architecture/core/DOMAIN_OBJECTS.md b/docs/architecture/core/DOMAIN_OBJECTS.md
index 597702c79..4bee38191 100644
--- a/docs/architecture/core/DOMAIN_OBJECTS.md
+++ b/docs/architecture/core/DOMAIN_OBJECTS.md
@@ -93,9 +93,19 @@ Value Objects MUST NOT:
• contain business workflows
• reference entities
• perform IO
+ • **contain presentation or formatting logic** (e.g., `formatForDisplay()`)
⸻
+### Domain vs. Presentation Value Objects
+
+It is critical to distinguish between **Domain Value Objects** and **Presentation Value Objects** (Displays):
+
+- **Domain Value Objects (Core):** Protect business meaning and invariants. They are pure and know nothing about how data is shown to users.
+- **Display Objects (Website):** Protect presentation logic and formatting. They are used by the Website (both server and client) to ensure consistent display.
+
+If the API needs to return formatted strings, this logic belongs in **API Presenters**, not in Domain Value Objects.
+
Creation Rules
• create() for new domain meaning
• fromX() for interpreting external formats
diff --git a/docs/architecture/website/DISPLAY_OBJECTS.md b/docs/architecture/website/DISPLAY_OBJECTS.md
index 0fd4ccfed..523bb172b 100644
--- a/docs/architecture/website/DISPLAY_OBJECTS.md
+++ b/docs/architecture/website/DISPLAY_OBJECTS.md
@@ -11,6 +11,13 @@ In this codebase, a Display is a **Frontend Value Object**:
- deterministic
- side-effect free
+### Distinction from Domain Value Objects
+
+While both are "Value Objects", they serve different layers:
+
+1. **Domain Value Objects (Core):** Encapsulate business truth and invariants (e.g., `Money`, `EmailAddress`). They are pure and never contain formatting logic.
+2. **Display Objects (Website):** Encapsulate presentation truth and formatting (e.g., `PriceDisplay`, `DateDisplay`). They are used to transform raw data or Domain Value Objects into user-ready strings.
+
It answers the question:
> “How should this specific piece of information be shown?”
@@ -65,6 +72,8 @@ A Display MUST NOT:
- influence system behavior
- be sent back to the server
- depend on backend or infrastructure concerns
+- **depend on environment-specific APIs** (e.g., `window`, `document`, `navigator`)
+- **be serialized** (they are classes; only their primitive outputs are stored in `ViewData`)
In this repository, a Display MUST NOT:
@@ -73,6 +82,25 @@ In this repository, a Display MUST NOT:
Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches.
+### Handling Client-Only Formatting
+
+If a formatting requirement **strictly requires** client-only APIs (e.g., browser-native relative time or local timezone detection):
+
+1. It MUST NOT live in a `Display Object`.
+2. It SHOULD live in a **View Model** (which is client-only).
+3. The Template should handle the transition from server-provided `ViewData` to client-updated `ViewData`.
+
+### Best Practices for Time/Date Formatting
+
+To avoid hydration mismatches while still providing good SEO and UX:
+
+1. **Use UTC methods for Determinism:** In `Display Objects`, prefer `getUTCDate()`, `getUTCMonth()`, etc., over their local counterparts. This ensures the server and client produce the exact same string regardless of their local timezones.
+2. **Hardcoded Arrays:** Use hardcoded arrays for month/day names instead of `Intl` to ensure consistency across environments.
+3. **Pass "Now" as an Argument:** For relative time (e.g., "time ago"), pass the reference "now" timestamp as an argument to the `Display Object` instead of calling `Date.now()` inside it.
+4. **The "Upgrade" Pattern:**
+ - **Server:** `ViewData Builder` uses a `Display Object` to produce a deterministic UTC-based string (e.g., "2024-01-18 15:00 UTC").
+ - **Client:** `View Model` uses client APIs (`Intl`, `toLocale*`) to produce a localized string (e.g., "3:00 PM") and updates the `ViewData`.
+
## Localization rule (strict)
Localization MUST NOT depend on runtime locale APIs.
@@ -105,16 +133,17 @@ Placement rule (strict):
---
-## Relationship to View Models
+## Relationship to View Models and ViewData Builders
-- View Models MAY use Displays
-- Displays MUST NOT depend on View Models
-- Displays represent **parts**
-- View Models represent **screens**
+Displays are the **shared source of truth** for formatting logic across the website:
+
+- **ViewData Builders (Server):** Use Displays to produce deterministic, formatted strings for SEO and initial SSR.
+- **View Models (Client):** Use Displays to produce formatted strings for interactive UI and client-specific context.
Additional strict rules:
- View Models SHOULD compose Displays.
+- ViewData Builders SHOULD use Displays for all formatting.
- Displays MUST NOT be serialized or passed across boundaries.
- They must not appear in server-to-client DTOs.
- Templates should receive primitive display outputs, not Display instances.
@@ -138,9 +167,17 @@ Additionally:
## Summary
-- Displays encapsulate **how something looks**
-- View Models encapsulate **what a screen needs**
-- Both are presentation concerns
-- Neither contains business truth
+- Displays encapsulate **how something looks** (the single source of truth for formatting logic).
+- View Models encapsulate **what a screen needs** (including client-specific "last mile" formatting).
+- Both are presentation concerns.
+- Neither contains business truth.
-In one sentence: Displays are **Value Objects for UI display**, not utility functions.
\ No newline at end of file
+In one sentence: **Displays are the shared source of truth for deterministic formatting logic, used by both the server and the client.**
+
+---
+
+## Final Rule: Where does formatting go?
+
+1. **Is it deterministic?** (e.g., currency symbols, fixed date formats, labels) → **Display Object**.
+2. **Is it client-only?** (e.g., `Intl.*`, `toLocale*`, browser timezone) → **View Model**.
+3. **Is it for SEO?** → **ViewData Builder** (using a Display Object).
\ No newline at end of file
diff --git a/docs/architecture/website/VIEW_DATA.md b/docs/architecture/website/VIEW_DATA.md
index 413792736..fed684e72 100644
--- a/docs/architecture/website/VIEW_DATA.md
+++ b/docs/architecture/website/VIEW_DATA.md
@@ -43,15 +43,22 @@ useEffect(() => {
setViewModel(vm);
}, []);
-// Template receives ViewModel, not ViewData
-return viewModel ? : null;
+// Template receives ViewData from ViewModel
+return viewModel ? : null;
```
Templates MUST NOT compute derived values.
ViewData Builders MUST NOT call the API.
-## 4) Determinism rules
+## 4) Formatting and SEO
+
+ViewData is responsible for providing **fully formatted strings** to Templates for Server-Side Rendering (SSR).
+
+- **SEO Requirement:** All data required for search engines (prices, dates, counts, labels) MUST be formatted in the `ViewData Builder` on the server.
+- **Template Simplicity:** Templates should simply render the strings provided in `ViewData` without further processing.
+
+## 5) Determinism rules
Any formatting used to produce ViewData MUST be deterministic.
diff --git a/docs/architecture/website/VIEW_MODELS.md b/docs/architecture/website/VIEW_MODELS.md
index e58de2f09..ec66796ff 100644
--- a/docs/architecture/website/VIEW_MODELS.md
+++ b/docs/architecture/website/VIEW_MODELS.md
@@ -20,10 +20,17 @@ A View Model MAY:
- accept an API DTO as input
- derive UI-specific fields
- combine or reshape data for rendering
-- perform formatting (dates, numbers, labels)
+- perform formatting (dates, numbers, labels), especially for **client-only context** (e.g., local timezone, relative "time ago")
- handle localization and presentation logic
- use Display Objects for reusable UI concerns
+### Formatting Responsibility
+
+While `ViewData Builders` handle formatting for SEO and initial render, `View Models` are responsible for:
+- **Client-specific formatting:** Data that depends on the browser's locale, timezone, or precise location.
+- **Interactive formatting:** Updating display values in response to user input or state changes.
+- **Consistency:** Using the same `Display Objects` as the server to ensure a consistent look and feel.
+
In the website SSR/RSC architecture, View Models MAY compute view-only derived values, but MUST NOT be the type passed into Templates.
A View Model MUST: