Routes are declared inside .gwdk files. File location does not define route
identity.
Page Routes
Every current page file must declare a route. guard is optional, but a page
is not public by default: a guardless page warns (missing_page_guard) and its
route is denied (403) until guard public (or a protective guard) is added.
See guards.md for the full access contract. Page ID
derives from the filename unless page is present:
route "/"
guard public
view {
<main>
<h1>Home</h1>
</main>
}
Use explicit page only when page identity should not follow the filename.
Current route rules:
- Routes must start with
/. /is the only route that may end with/.- Routes must not include query strings, fragments, backslashes, whitespace,
control characters, empty segments,
., or... - Dynamic params must be whole path segments, such as
/blog/{slug}. - Param names use
[A-Za-z_][A-Za-z0-9_]*. - A route cannot repeat the same param name.
- Duplicate page route patterns are rejected.
/blog/{slug}and/blog/{id}are the same route pattern, and/docs/{path...}and/docs/{rest...}are the same route pattern. - Generated app request matching and SPA file lookup reject request paths that
contain empty,
., or..segments before cleaning. Encoded dot segments such as%2e%2etherefore return 404 instead of resolving to a different generated route or asset.
Rest Params
A page route may declare one rest (catch-all) param as its final segment:
route "/docs/{path...}"
guard public
go server {}
view {
<main>
<h1>{param("path")}</h1>
</main>
}
Rest param contract:
{name...}is allowed only as the final route segment. A rest param before the end of the route is rejected with amalformed_routediagnostic.- A rest param matches one or more remaining request path segments.
/docsdoes not match/docs/{path...};/docs/introand/docs/guides/routingdo. Each matched segment still rejects empty,., and..values. - The captured value is the remaining segments joined with
/, for exampleguides/routing. Read it withparam("name")in the view, or in request-time Go throughapp.Params(ctx)androute.Required(params, "name"). - Rest params are always strings. Typed rest params such as
{path...:int}are rejected. - Rest params require request-time (SSR) rendering when used on page routes,
because build-time SPA paths cannot enumerate and escape multi-segment
values. Declare
server {}orgo server {}on the page. - Rest params are supported on request-time fragment endpoint routes. Action, API, and Go comment endpoint paths reject rest params. An action or API that omits its path inherits the page route, so inline endpoints on a rest page are rejected the same way unless they declare their own concrete path.
- Rest routes participate in ambiguity validation:
/docs/{path...}overlaps/docs/{slug},/docs/{section}/{slug}, and concrete routes such as/docs/guides/intro, so those combinations are rejected asambiguous_dynamic_route. The same applies to a same-method endpoint inside a rest route's namespace (for example aGETAPI at/docs/guides/introbeside/docs/{path...}), which would otherwise shadow part of the page.
Unsupported route forms today:
- Optional params such as
/docs/{slug?}. The diagnostic is explicit: optional route parameters are not supported; declare explicit routes for each shape (rest parameters{name...}are supported as the final segment). - Route groups that affect URL shape independently from explicit
route. - Page/API same-path content negotiation. A page route and endpoint may share a
path only when their HTTP methods do not conflict, such as
GET /signuppage plusPOST /signupaction.
Trailing Slash Policy
Routes are canonical without a trailing slash. The policy is explicit:
- Declarations: omit trailing slashes except for
/.route "/blog/hello/"is rejected withmalformed_route. - Requests: generated servers respond to
GETandHEADrequests whose path carries a trailing slash (and is not/) with a308 Permanent Redirectto the cleaned canonical path, preserving the query string.GET /blog/hello/?page=2redirects to/blog/hello?page=2instead of serving duplicate content. POSTbehavior is unchanged: generated action handlers tolerate a trailing slash on concrete POST routes as a compatibility fallback and redirect to the declared target when configured.
Pages may declare response cache intent with cache. The value is carried as
route metadata and should be a literal HTTP Cache-Control value:
route "/docs"
guard public
cache "public, max-age=60"
Pages may also declare stale-while-revalidate behavior with revalidate.
Values may be whole seconds or Go-style whole-second durations such as 60s,
5m, or 1h. revalidate requires cache and appends a concrete
stale-while-revalidate=<seconds> directive to the generated Cache-Control
header:
route "/docs"
guard public
cache "public, max-age=60"
revalidate 5m
Generated binaries apply explicit page cache values to successful static SPA
HTML and SSR HTML responses. When revalidate is present, generated binaries
send the appended stale-while-revalidate directive for the same successful
responses. Request-time safety policies still win for actions, APIs, partial
responses, SSR load redirects, CSRF HTML mutation, and generated request-time
errors; those use no-store.
SPA Routes
SPA render is the default:
route "/docs"
guard public
view {
<main>
<h1>Docs</h1>
</main>
}
gowdk build --out <dir> writes the route as spa HTML. For /docs, the
current output is <dir>/docs/index.html. For /, the output is
<dir>/index.html.
When a SPA page, layout, or referenced component contains a literal internal
link such as <a href="/docs">, the build emits the small
assets/gowdk/gowdk.js enhancement runtime. That runtime intercepts normal
same-origin link clicks, fetches the real generated HTML page, replaces the
current document head/body, updates browser history, and preserves focus/scroll
where possible. It does not define routes or decide whether a route exists; the
generated files or generated server remain the source of truth, and direct page
open/refresh must keep working.
Dynamic SPA Routes
Dynamic SPA routes require paths {}. Action endpoints on a dynamic SPA page
inherit that page's generated concrete paths:
route "/blog/{slug}"
guard public
paths {
=> { slug: "hello-gowdk" }
=> { slug: "compile-first" }
}
view {
<main>
<h1>{slug}</h1>
<p>{param("slug")}</p>
</main>
}
The implemented paths {} subset accepts literal string records. Route params
from those records are available to the current spa interpolation scope and
to literal build {} string interpolation.
Build:
gowdk build --out /tmp/gowdk-dynamic examples/pages/blog-post.page.gwdk
Generated output:
/tmp/gowdk-dynamic/blog/hello-gowdk/index.html
/tmp/gowdk-dynamic/blog/compile-first/index.html
Imported and same-package Go build functions can receive the current
paths {} route params by declaring one gowdk.BuildParams argument.
Action Endpoints
An act declaration on a page adds a POST endpoint in the current generated
app slice:
package signup
route "/signup"
guard public
act Submit POST "/signup"
view {
<form g:post={Submit}>
<input name="email" required />
<button type="submit">Sign up</button>
</form>
}
App-shell HTML lowers g:post={Submit} to a normal POST form. Generated apps
built with --app --bin serve concrete action endpoints. If the same directory
as the .gwdk file contains an exported Go function with the exact declared
symbol, the generated handler calls it when it uses one of these signatures:
func Submit(context.Context) (response.Response, error)
func Submit(context.Context, SignupInput) (response.Response, error)
func Submit(context.Context, *SignupInput) (response.Response, error)
func Submit(context.Context, form.Values) (response.Response, error)
Missing or unsupported functions generate HTTP 501 handlers.
Actions can also be declared on the exported Go handler itself:
//gowdk:act POST /signup
func Submit(context.Context, SignupInput) (response.Response, error)
Go comment action endpoints are standalone backend endpoints. They use the same
binding and generated adapter pipeline as .gwdk action declarations, but they
do not infer page-local form schemas, fragments, or guards from .gwdk page
markup.
Malformed //gowdk: comments, unknown endpoint kinds, and missing method or
path fields fail with malformed_go_endpoint_comment.
Generated action and web-command handlers validate CSRF tokens by default before
generated decoding or user handlers run. Missing or invalid tokens return HTTP
403 with invalid csrf token and Cache-Control: no-store.
For a broader runnable action cookbook, see examples/endpoints/src/endpoints/contact.page.gwdk
and examples/endpoints/src/endpoints/settings.page.gwdk.
API Routes
API endpoint metadata is parsed, appears in route plans, and can bind to same-package Go handlers:
package api
route "/status"
guard public
api Health GET "/api/health"
view {
<main>
<h1>Status</h1>
</main>
}
Supported methods today: GET, POST, PUT, PATCH, and DELETE.
api Health GET "/api/health" maps to exported Go function Health in the
same package as the .gwdk file when the function has signature
func(context.Context, *http.Request) (response.Response, error). Missing or
unsupported functions generate HTTP 501 handlers.
APIs can also be declared on the exported Go handler itself:
//gowdk:api GET /api/health
func Health(context.Context, *http.Request) (response.Response, error)
The compiler discovers Go endpoint comments only in selected source packages,
does not infer endpoints from function names, and does not scan framework route
registrations. If a Go comment endpoint and a .gwdk endpoint declare the same
method/path pair, validation fails with a route conflict diagnostic.
For session, search, JSON CRUD, and webhook API examples, see
examples/endpoints/src/endpoints/api.page.gwdk.
SSR Routes
SSR is optional and must be enabled for validation:
gowdk check --ssr examples/ssr/simple-ssr.page.gwdk
First-slice concrete and dynamic request-time SSR pages with declared server {}
fields can be
generated into an embedded app and binary:
gowdk build --ssr --out /tmp/gowdk-ssr-build \
--app /tmp/gowdk-ssr-app \
--bin /tmp/gowdk-ssr-site \
examples/ssr/dynamic-ssr.page.gwdk
Dynamic SSR route params render through generated placeholders and request-time
HTML escaping. Dynamic fragment endpoint params are attached to fragment hook
contexts. Params can be declared as {name}, {name:type}, or — as the final
segment only — {name...} (always a string). Supported types are string,
int, int64, uint, uint64, bool, and float64. Generated SSR handlers
attach route metadata through runtime/app.Route(ctx), including render mode,
guard IDs, dynamic params, typed route-param metadata, and declared layouts.
Generated SSR and fragment handlers attach raw dynamic params through
runtime/app.Params(ctx) and decoded typed params through
runtime/app.TypedParams(ctx).
There are no generated per-route param struct types yet. Request-time user code
should use app.Params(ctx), app.TypedParams(ctx), or the runtime/route
typed helpers. Per-route structs may be added later only if the generated API
stays stable and simpler than the current helpers.
User Go can still decode raw params with runtime/route helpers:
params := app.Params(ctx)
id, ok, err := route.Int(params, "id")
if err != nil {
return response.HTMLBody(400, "invalid route param"), err
}
if !ok {
return response.HTMLBody(404, "missing route param"), nil
}
_ = id
The helpers support String, Int, Int64, Uint, Uint64, Bool, and
Float64. Required returns a missing-param error when a required param is not
present. Decode errors name the param and expected type without echoing the raw
request value. Generated typed SSR and fragment bindings return 400 for
invalid typed route params and 404 for missing route params before guards,
page rendering, or fragment hooks run.
For standalone fragment and action-driven partial examples, see
examples/endpoints/src/endpoints/fragments.page.gwdk.
Endpoint user code can read generated endpoint metadata with
runtime/app.Endpoint(ctx). This is the stable accessor for action, API, and
fragment handler metadata today. Typed load-result and action-result data
accessors are deferred until those result contracts are stable.
server { => { field, user.name } } execution calls same-package Go
Load<PageID> functions at request time through ssr.LoadContext. Returned
declared identifiers and dotted paths are resolved from nested maps with string
keys, structs, pointers, interfaces, exported Go field names, and json tag
names, then HTML-escaped into generated placeholders.
Route Plans
Use gowdk routes to inspect validated route and endpoint metadata:
gowdk routes --ssr examples/pages/*.gwdk examples/actions/*.gwdk examples/partials/*.gwdk examples/api/*.gwdk examples/ssr/*.gwdk
gowdk endpoints --ssr examples/actions/*.gwdk examples/api/*.gwdk
The current JSON schema is version 1. routes is the generated route table:
it includes page/file route kinds such as static, spa, ssr, and hybrid,
plus generated endpoint route rows for actions, APIs, fragments, and routable
g:command/g:query contract references. Route records include the HTTP
method/path, page ID, .gwdk package, guards, source file, source span, and
planned handler. Page rows also include effective render mode, layouts, dynamic
route params, and cache policy when declared. Endpoint rows include
endpointSource, directive (act, api, fragment, g:command, or
g:query), symbol/contract name, no-store backend cache policy, CSRF
applicability, route params when declared, and binding status when known.
endpoints contains the detailed framework-neutral backend endpoint records
for the same action, API, fragment, and contract adapter routes. Endpoint
records repeat the route table fields and add Go package path/name when known,
backend binding details, and contract binding details. Backend binding details
repeat the Go package name, import path when known, handler symbol, and
supported signature/input metadata when the handler is bound. Contract binding
details include the contract kind, reference name, binding status, local input
type, result type, roles, handler, register function, and message when known.
The
info list reports disabled route-mode lanes, for example SSR disabled on a
SPA route.
gowdk endpoints prints the same versioned endpoint records without page route
records or info notes. Route/method conflicts remain hard validation
diagnostics; gowdk check --json reports the conflicting declaration with a
source range and a related location for the first declaration when available.
Use gowdk inspect ir when route debugging needs the full typed compiler IR
instead of the route-report schema. The IR output is for M2 compiler debugging
and snapshots; keep gowdk routes for route and endpoint report integrations.
Use gowdk inspect endpoint-graph --json when route debugging needs the
dispatch graph. The current graph schema is version 1 and includes page,
route, endpoint, handler, guard, contract, and structural nodes.
Edges describe route declarations, page-owned endpoints, planned/generated
handlers, inherited guard policy, routable command/query contract references,
and structural dispatches links from g:post, g:command, and g:query
view elements to generated endpoint nodes. Endpoint nodes carry method/path,
source kind, cache, guards, CSRF policy, binding status, signature, input type,
and source span when available.
Use gowdk inspect asset-graph --json when debugging which source owner
introduced CSS, JavaScript, component file assets, or WASM declarations. The
current graph schema is version 1 and includes owner, template, and asset
nodes with deterministic has_template, declares_asset, and uses_package
edges.
Use gowdk inspect tree --json when source navigation needs the unified node
tree instead of a route-only projection. The current tree schema is version 1
and includes package, page, component, layout, route, endpoint,
contract-reference, view, element, component-call, and text nodes with stable
IDs, source paths, source spans when known, properties, and ordered children.
Resolved component calls also produce renders_component edges, and component
composition cycles are reported as tree diagnostics.