Views
In v2 a view is an Inertia page component. Go handlers prepare a page's props; React, Vue or Svelte renders the page in the browser, with optional server-side rendering.
Templ still has two jobs: the Inertia root document at resources/root.templ, and email templates under email/. There is no Templ page layer.
Render a page
A handler names the component and passes serializable props:
1func (h ProductHandler) Index(ctx context.Context) error {
2 page, err := anduin.QueryInt(ctx, "page", 1)
3 if err != nil {
4 return anduin.BadRequest(err)
5 }
6
7 result, err := h.products.Paginate(ctx, int64(page), 25)
8 if err != nil {
9 return err
10 }
11
12 return h.renderer.Page(ctx, "Products/Index", inertia.Props{
13 "items": toProductResources(result.Items),
14 "totalPages": result.TotalPages,
15 }).Render()
16}
The component name is resolved against resources/js/Pages, so Products/Index is resources/js/Pages/Products/Index.tsx (or .vue / .svelte). Keep one component per page and let a shared Layout own the frame.
Props must be ordinary JSON values. Build a small resource struct with stable JSON tags at the boundary; never pass a database entity, a generated query row, or a pgtype value to a page. If a struct is shared by several pages, inertia.FromStruct turns its exported fields into props.
Write the component
1import Layout from '@/Layouts/Layout'
2
3type ProductResource = {
4 id: string
5 name: string
6}
7
8type Props = {
9 items: ProductResource[]
10 totalPages: number
11}
12
13export default function Index({ items, totalPages }: Props) {
14 return (
15 <Layout>
16 <h1>Products</h1>
17 <p>{items.length} products across {totalPages} pages</p>
18 </Layout>
19 )
20}
Page components are application code. Props passed as a literal inertia.Props map are not generated, so type them beside the component. andurel sync payloads generates form payload types from named DecodeJSON/DecodeForm structs and, when a handler passes a handler-owned struct to inertia.FromStruct, the page prop type too.
Links and form submissions use the generated route helpers:
1import { useForm } from '@inertiajs/react'
2import { routes } from '@/routes'
3import type { CreateProductPayload } from '@/types/payloads'
4
5export default function Create() {
6 const form = useForm<CreateProductPayload>({ name: '', priceCents: 0 })
7
8 return (
9 <form
10 onSubmit={(event) => {
11 event.preventDefault()
12 form.post(routes.productIndex())
13 }}
14 >
15 <input
16 value={form.data.name}
17 onChange={(event) => form.setData('name', event.target.value)}
18 />
19 <button type="submit" disabled={form.processing}>Create</button>
20 </form>
21 )
22}
Layouts, components and the head
resources/js/Layouts/ holds the frame shared by pages, and resources/js/Components/ holds reusable UI. Layouts and page components are plain React, Vue or Svelte components; they are not generated and they are not shared with Go.
Use Inertia's <Head> for page titles and metadata. The renderer shares appUrl, siteName, environment, appVersion, hosts and the visitor's auth state with every page, so a layout can build canonical URLs and switch between sign-in and sign-out without each handler passing those values.
Validation errors arrive as the errors page prop and as the errors field on the form helper. Render them next to the field:
1import { usePage } from '@inertiajs/react'
2
3function EmailError() {
4 const { errors } = usePage<{ errors: Record<string, string> }>().props
5 if (!errors.email) {
6 return null
7 }
8
9 return <p className="text-sm text-destructive">{errors.email}</p>
10}
The root document
resources/root.templ is the one page-shaped templ in the project. It receives inertia.RootData and is responsible for the document shell: the container element, the embedded page JSON for the first visit, the Vite head and body tags, and the mount script.
Keep the root document generic. Page-specific titles and metadata belong in the page's <Head>, not in the root, or a page will render two titles under SSR.
Assets, SSR and production
andurel run starts the Vite dev server and proxies development assets. andurel build compiles Tailwind, the client bundle, the SSR bundle and the Go binary.
SSR is an option on a single page render:
1return h.renderer.Page(ctx, "Products/Index", props).SSR().Render()
- In development the render is proxied through the Vite dev server, so use
andurel runrather than a barego run ./cmd/app. - In production
cmd/ssrruns the Node renderer; the app reaches it atINERTIA_SSR_URL. - Managed SSR requires Node.js 22 or newer.
config/inertia.godefaultsINERTIA_SSR_FAIL_FASTtotrue, so when fail-fast is on, a page that opts into SSR fails instead of silently rendering only on the client when the renderer is unavailable. The shipped.env.examplesets it tofalsefor local development, where no SSR process runs; set it deliberately in production.
Pages without .SSR() render client-side from the embedded page object. Both paths use the same components and the same props.
What replaced Templ fragments
v1 pages could return Templ fragments and patch them with Datastar. v2 has no fragment layer. An interaction is either frontend state inside the page component or an ordinary request that returns a new page, a redirect, or JSON. Choose the page boundary deliberately: a page render for navigation, a JSON endpoint for data.
For the protocol details, see Inertia. For the adapter and package-manager choice, see Frontend Options.