place

Routes as values

Every page() call produces a value. That value carries its path, its action callers, and its types. Move the file, the value moves; references stay sound because they point at the value, not the file path.

Declare

ts
// src/pages/post.page.tsx import { page } from '@place/component' const postPage = page('/posts/:id', { view: ({ id }) => <Article id={id} />, }) export default postPage // a value

Register

Pages register through an explicit array. No file-system convention, no "must live in this folder", no page.tsx vs route.tsx guessing.

ts
// src/app.ts import post from './pages/post.page' app({ pages: [home, post, archive], // explicit array, in any order /* ... */ }).run()
ts
import postPage from './pages/post.page' // Anywhere in your app: <Link to={postPage} params={{ id: '42' }}>read post</Link> // Type error if you forget params, pass the wrong name, or move the path: <Link to={postPage} params={{ slug: '42' }} /> // TS error: slug not in params

<Link to={postPage} /> works because the page value carries its path AND its param shape. If you rename the path or change the params, every link, every action caller, every navigate() call gets a TypeScript error — the rename catches them.

Call actions through the value

ts
// post.page.tsx const postPage = page('/posts/:id', { on: { save: async (input: { title: string }, { params }) => { /* ... */ }, }, view: () => ( <button onClick={() => postPage.save({ title: 'New' })}>save</button> ), })

The page value carries its action methods directly. postPage.save({ ... }) is fully typed; the underlying fetch targets /posts/:id/_action/save with the current URL's :id resolved.

Move a file, nothing breaks
Because the route lives in the value, file moves are invisible to consumers. Move post.page.tsx from pages/ to features/blog/pages/ — update the import, every other line still works.

What this rules out

place doesn't do file-system routing. Two pages with the same path throw on register. There's no auto-discovery — if a page isn't in the array, it isn't served. Both are intentional: the routes table is exactly the array you wrote.

See also