원클릭으로
controller-views
Creating controller-returned views with route overrides, multi-framework support, and the html tagged template in Rudder
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Creating controller-returned views with route overrides, multi-framework support, and the html tagged template in Rudder
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | controller-views |
| description | Creating controller-returned views with route overrides, multi-framework support, and the html tagged template in Rudder |
| license | MIT |
| appliesTo | ["@rudderjs/view"] |
| trigger | creating files in `app/Views/`, returning `view('id', props)` from a route handler, or overriding the view-id → URL mapping |
| skip | working with Vike `+Page.tsx` / `+config.ts` directly — that's outside `app/Views/` |
| metadata | {"author":"rudderjs"} |
Load this skill when you need to create controller-returned views using view('id', props), override route mappings, build vanilla HTML views with auto-escaping, or understand the multi-framework view pipeline.
ViewResponse that the server adapter (@rudderjs/server-hono) detects and renders through Vike's SSR pipeline.view('dashboard') -> URL /dashboard, view('admin.users') -> /admin/users. Dots become path separators, the id is lowercased.const route = '/custom-path' at the top of a view file to decouple the URL from the id-derived default.app/Views/<PascalCaseId>.tsx (or .vue, .ts for vanilla).@rudderjs/vite scans app/Views/ at build/dev time and generates Vike pages under /__view/<id>.vike-react, vike-vue, or vike-solid. Only one renderer can be installed at a time..ts files that return strings. Use html tagged template for auto-escaping.pageContext.json fetches -- no full page reloads.// app/Views/Dashboard.tsx
import { usePageContext } from 'vike-react/usePageContext'
export default function Dashboard() {
const { viewProps } = usePageContext() as { viewProps: { users: any[] } }
return (
<div>
<h1>Dashboard</h1>
<p>{viewProps.users.length} users</p>
</div>
)
}
// routes/web.ts
import { Route } from '@rudderjs/router'
import { view } from '@rudderjs/view'
Route.get('/dashboard', async () => {
const users = await User.all()
return view('dashboard', { users })
})
Route.get('/about', async () => {
return view('about', { version: '1.0.0' })
})
When the controller URL doesn't match the id-derived path, add export const route to the view file:
// app/Views/Welcome.tsx
// Without this, the id 'welcome' would map to /welcome
// But we want it at /
export const route = '/'
export default function Welcome() {
return <h1>Welcome to Rudder</h1>
}
// app/Views/Auth/Login.tsx
export const route = '/login'
export default function Login() {
// ...
}
The scanner picks up the export const route and generates the correct Vike route table entry. Without this, SPA navigation between controller views falls back to full page reloads.
| View ID | File path | Default URL |
|---|---|---|
'dashboard' | app/Views/Dashboard.tsx | /dashboard |
'about' | app/Views/About.tsx | /about |
'admin.users' | app/Views/Admin/Users.tsx | /admin/users |
'auth.login' | app/Views/Auth/Login.tsx | /auth/login |
// Controller-side
Route.get('/users/:id', async (req) => {
const user = await User.find(req.params.id)
if (!user) return { status: 404 }
return view('users.show', { user: user.toJSON() })
})
// app/Views/Users/Show.tsx
import { usePageContext } from 'vike-react/usePageContext'
export default function UserShow() {
const { viewProps } = usePageContext() as { viewProps: { user: Record<string, unknown> } }
const { user } = viewProps
return <h1>{user.name as string}</h1>
}
For pages that don't need interactivity -- admin reports, emails, static content:
// app/Views/AdminReport.ts
import { html } from '@rudderjs/view'
interface AdminReportProps {
title: string
rows: { name: string; total: number }[]
}
export default function AdminReport({ title, rows }: AdminReportProps): string {
return html`
<h1>${title}</h1>
<table>
<thead><tr><th>Name</th><th>Total</th></tr></thead>
<tbody>
${rows.map(r => html`<tr><td>${r.name}</td><td>${r.total}</td></tr>`)}
</tbody>
</table>
`.toString()
}
The html tagged template:
escapeHtml()SafeString: passed through unchanged (nested html calls return SafeString)import { escapeHtml } from '@rudderjs/view'
export default function Page({ userInput }: { userInput: string }): string {
return `<div>${escapeHtml(userInput)}</div>`
}
import { html, SafeString } from '@rudderjs/view'
const trustedMarkdown = new SafeString(renderedMarkdownHtml)
export default function Page(): string {
return html`
<article>${trustedMarkdown}</article>
`.toString()
}
// app/Views/Welcome.tsx
export const route = '/'
import { usePageContext } from 'vike-react/usePageContext'
export default function Welcome() {
const ctx = usePageContext() as { viewProps: { user?: { name: string } } }
const user = ctx.viewProps.user
return (
<div>
<h1>Welcome to Rudder</h1>
{user ? (
<p>Signed in as {user.name} <a href="/auth/logout">Sign out</a></p>
) : (
<nav>
<a href="/login">Log in</a>
<a href="/register">Register</a>
</nav>
)}
</div>
)
}
Packages that ship views follow this structure:
packages/my-package/
views/
react/
MyComponent.tsx
vue/
MyComponent.vue
src/
routes.ts # exports registerMyRoutes(router, opts)
// packages/my-package/src/routes.ts
import type { Router } from '@rudderjs/router'
import { view } from '@rudderjs/view'
export function registerMyRoutes(router: Router, opts = {}): void {
router.get('/my-page', async () => view('my-package.page', {}))
}
Consumer apps vendor the views into app/Views/ via vendor:publish.
See playground/app/Views/Welcome.tsx for the default landing page, playground/app/Views/Auth/ for vendored auth views, and playground/routes/web.ts for route registration.
/login but the view file has no export const route = '/login', the scanner derives /auth/login from the id auth.login. The browser URL and Vike's route table don't match, so SPA nav falls back to full page reloads. Always add export const route when the URL diverges from the id.vike-react / vike-vue / vike-solid. Having multiple triggers a "multi-renderer installed" error from the scanner.view() function alone doesn't discover files. The @rudderjs/vite plugin's scanner generates Vike pages at dev/build time.usePageContext().viewProps. The props are injected into Vike's pageContext by the ViewResponse.toResponse() method.string from their default export. Use .toString() on the SafeString returned by html.view('admin.users') expects app/Views/Admin/Users.tsx (PascalCase directories and filename).Setting up authentication with guards, sessions, registration, password reset, gates/policies, and vendor views in Rudder
Building MCP servers with tools, resources, prompts, decorators, and HTTP/stdio transports in Rudder
Creating Eloquent-style models, queries, relationships, casts, factories, and API resources in Rudder
Building AI agents with tools, streaming, conversation memory, approval flows, and middleware in Rudder
Defining server and client tools with Zod schemas, approval gates, streaming yields, and modelOutput for Rudder AI agents
Setting up authentication with guards, sessions, registration, password reset, gates/policies, and vendor views in Rudder