diff --git a/apps/website/eslint-rules/template-purity-rules.js b/apps/website/eslint-rules/template-purity-rules.js
index 6f43f93f4..1607db5cf 100644
--- a/apps/website/eslint-rules/template-purity-rules.js
+++ b/apps/website/eslint-rules/template-purity-rules.js
@@ -63,6 +63,38 @@ module.exports = {
},
},
+ // Rule 8: No 'use client' directive in templates
+ 'no-use-client-in-templates': {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'Forbid use client directive in templates',
+ category: 'Template Purity',
+ },
+ messages: {
+ message: 'Templates must not use "use client" directive - they should be stateless composition',
+ },
+ },
+ create(context) {
+ const filename = context.getFilename();
+ const isInTemplates = filename.includes('/templates/');
+
+ if (!isInTemplates) return {};
+
+ return {
+ ExpressionStatement(node) {
+ if (node.expression.type === 'Literal' &&
+ node.expression.value === 'use client') {
+ context.report({
+ node,
+ messageId: 'message',
+ });
+ }
+ },
+ };
+ },
+ },
+
// Rule 3: No computations in templates
'no-computations-in-templates': {
meta: {
diff --git a/apps/website/eslint-rules/ui-element-purity.js b/apps/website/eslint-rules/ui-element-purity.js
index df10bec0c..e83a1a7dd 100644
--- a/apps/website/eslint-rules/ui-element-purity.js
+++ b/apps/website/eslint-rules/ui-element-purity.js
@@ -35,11 +35,18 @@ module.exports = {
if (!isInUi) return {};
- let hasStateHooks = false;
- let hasEffectHooks = false;
- let hasContext = false;
-
return {
+ // Check for 'use client' directive
+ ExpressionStatement(node) {
+ if (node.expression.type === 'Literal' &&
+ node.expression.value === 'use client') {
+ context.report({
+ node,
+ messageId: 'noStateInUi',
+ });
+ }
+ },
+
// Check for state hooks
CallExpression(node) {
if (node.callee.type !== 'Identifier') return;
@@ -48,7 +55,6 @@ module.exports = {
// State management hooks
if (['useState', 'useReducer', 'useRef'].includes(hookName)) {
- hasStateHooks = true;
context.report({
node,
messageId: 'noStateInUi',
@@ -57,7 +63,6 @@ module.exports = {
// Effect hooks
if (['useEffect', 'useLayoutEffect', 'useInsertionEffect'].includes(hookName)) {
- hasEffectHooks = true;
context.report({
node,
messageId: 'noSideEffects',
@@ -66,7 +71,6 @@ module.exports = {
// Context (can introduce state)
if (hookName === 'useContext') {
- hasContext = true;
context.report({
node,
messageId: 'noStateInUi',
@@ -76,8 +80,8 @@ module.exports = {
// Check for class components with state
ClassDeclaration(node) {
- if (node.superClass &&
- node.superClass.type === 'Identifier' &&
+ if (node.superClass &&
+ node.superClass.type === 'Identifier' &&
node.superClass.name === 'Component') {
context.report({
node,
@@ -90,7 +94,7 @@ module.exports = {
AssignmentExpression(node) {
if (node.left.type === 'MemberExpression' &&
node.left.property.type === 'Identifier' &&
- (node.left.property.name === 'state' ||
+ (node.left.property.name === 'state' ||
node.left.property.name === 'setState')) {
context.report({
node,
diff --git a/docs/architecture/website/REACT_COMPONENT_ARCHITECTURE.md b/docs/architecture/website/REACT_COMPONENT_ARCHITECTURE.md
new file mode 100644
index 000000000..6e003ef87
--- /dev/null
+++ b/docs/architecture/website/REACT_COMPONENT_ARCHITECTURE.md
@@ -0,0 +1,254 @@
+# React Component Architecture (Concept)
+
+This document defines the clean concept for React component architecture in `apps/website`.
+
+## Core Principle
+
+**Separation of concerns by responsibility, not just by file location.**
+
+## The Four Layers
+
+### 1. App Layer (`app/`)
+**Purpose**: Entry points and data orchestration
+
+**What lives here**:
+- `page.tsx` - Server Components that fetch data
+- `layout.tsx` - Root layouts
+- `route.tsx` - API routes
+- `*PageClient.tsx` - Client entry points that wire server data to client templates
+
+**Rules**:
+- `page.tsx` does ONLY data fetching and passes raw data to client components
+- `*PageClient.tsx` manages client state and event handlers
+- No UI rendering logic (except loading/error states)
+
+**Example**:
+```typescript
+// app/teams/page.tsx
+export default async function TeamsPage() {
+ const query = new TeamsPageQuery();
+ const result = await query.execute();
+
+ if (result.isErr()) {
+ return