Pages

Forge pages are Svelte components selected by UI OpenAPI routes. The x-view path points to a page relative to client.componentDir, and optional x-layout entries wrap that page from outside to inside.

src/ui/openapi/pages/dashboard.yaml
# src/ui/openapi/pages/dashboard.yaml
modules:
  dashboard:
    basePath: /dashboard
    x-layout:
      - layout/app_layout.svelte
      - layout/dashboard_layout.svelte
    paths:
      /:
        get:
          summary: Dashboard home
          x-view: pages/dashboard/main.svelte
      /reports/{id}:
        get:
          summary: Report detail
          x-view: pages/dashboard/report.svelte
src/ui/pages/dashboard/main.svelte
<!-- src/ui/pages/dashboard/main.svelte -->
<script lang="ts">
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

<h1>Dashboard</h1>
<button onclick={() => count++}>
  Count: {count} ({doubled})
</button>

Route Metadata

  • x-view resolves the Svelte page.
  • x-layout wraps the page with one or more layout components.
  • x-controller can attach a page controller for richer behavior.
  • Path values are inferred from route segments like /{id} and exposed to loaders and controllers.

Route Params and Query

Forge uses the frontend route definitions as the page router. A module basePath is prefixed to every path in that module, then route segments like /{projectId} become named params.

For /projects/42/tasks/7?tab=activity, Forge exposes params.projectId === "42", params.taskId === "7", and query.tab === "activity".

src/ui/openapi/pages/projects.yaml
# src/ui/openapi/pages/projects.yaml
modules:
  projects:
    basePath: /projects
    paths:
      /:
        get:
          summary: Project list
          x-view: pages/projects/list.svelte
      /{projectId}:
        get:
          summary: Project detail
          x-view: pages/projects/detail.svelte
          x-controller: controllers/project_detail.controller.svelte.ts
      /{projectId}/tasks/{taskId}:
        get:
          summary: Task detail
          x-view: pages/projects/task.svelte
src/ui/pages/projects/detail.load.ts
// src/ui/pages/projects/detail.load.ts
export default async function load({ params, query }) {
  const projectId = params.projectId;
  const selectedTab = query.tab ?? "overview";

  return {
    projectId,
    selectedTab
  };
}
src/ui/controllers/project_detail.controller.svelte.ts
import type { PageController, RouteContext } from "@noego/forge/client";

export default class ProjectDetailController implements PageController {
  data = $state({
    projectId: "",
    selectedTab: "overview"
  });

  initialize(loadData: { selectedTab?: string }, route: RouteContext) {
    this.data.projectId = route.params.projectId;
    this.data.selectedTab = loadData.selectedTab ?? route.query.tab ?? "overview";
  }
}

Where to Read Params

  • Use load({ params, query }) for data needed before render.
  • Use initialize(loadData, route) in page controllers for behavior state.
  • Use the page store in simple components that only need the current URL state.

Route Matching Rules

  • Params are strings. Parse numbers or booleans at the boundary that needs them.
  • Query values come from the URL search string, separate from path params.
  • Do not add OpenAPI parameters blocks for Forge page params; path segments define them.
src/ui/pages/projects/detail.svelte
<script lang="ts">
  import { page } from "@noego/forge/client";

  const projectId = $derived($page.params.projectId);
  const tab = $derived($page.query.tab ?? "overview");
</script>

<h1>Project {projectId}</h1>
<p>Current tab: {tab}</p>

Layouts

Layouts wrap your pages with common UI elements like headers, sidebars, and footers. Define layouts in your OpenAPI configuration and Forge will automatically nest them.

api.yaml
// ui/openapi/api.yaml - Define layouts in OpenAPI
paths:
  /admin/users:
    get:
      summary: Admin users page
      x-view: pages/admin/users.svelte
      x-layout:
        - layout/root_layout.svelte      # Outermost
        - layout/admin_layout.svelte     # Nested inside root
admin_layout.svelte
<!-- ui/layout/admin_layout.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';

  // Children are passed as a snippet
  let { children }: { children: Snippet } = $props();
</script>

<div class="admin-container">
  <aside class="sidebar">
    <nav>
      <a href="/admin/dashboard">Dashboard</a>
      <a href="/admin/users">Users</a>
      <a href="/admin/settings">Settings</a>
    </nav>
  </aside>

  <main class="content">
    <!-- Render nested content -->
    {@render children()}
  </main>
</div>

Snippet Children Pattern

Layouts receive their nested content as a Svelte snippet. Use @render children() to render the child content at the appropriate location in your layout.

NoEgo

© 2025 NoEgo. All rights reserved.