A unique feature of One is the ability to seamlessly route across a variety of route modes. What this means in plain english, is that you can choose whether each individual page within your routes uses one of a few strategies, such as rendering statically at build time, or rendering on-demand at request time.
One supports the following render modes, which are explained in detail below: api, spa, ssr, and ssg.
You can set your global render mode with the One vite plugin. The default is ssg.
vite.config.ts
import { one } from 'one/vite'
export default {
plugins: [
one({
web: {
defaultRenderMode: 'spa'
}
})
]
}
To specify the render mode on a per-page basis, you add a suffix to the filename, like so:
route+ssg.tsx - Matches /route, but will render the page as a SSG route.route+spa.tsx - Matches /route, but will render the page as a SPA route.route+ssr.tsx - Matches /route, but will render the page as a SSR route.route+api.tsx - Matches /route, but will render the page as an API route.You can also apply render modes to entire folders, and all routes within that folder will inherit the render mode. This is useful for organizing routes by their rendering strategy:
Terminal
app/
├── dashboard+ssr/ # All routes use SSR
│ ├── index.tsx # Uses SSR
│ ├── analytics.tsx # Uses SSR
│ └── settings+spa.tsx # Overrides to SPA
├── blog+ssg/ # All routes use SSG
│ ├── index.tsx # Uses SSG
│ └── [slug].tsx # Uses SSG
└── index.tsx # Uses vite config default
Render mode hierarchy:
page+spa.tsxdashboard+ssr/page.tsxdefaultRenderMode: 'ssg'Nested folders: Inner folder suffixes override outer folder suffixes:
Terminal
app/
└── dashboard+ssr/
├── index.tsx # Uses SSR
└── reports+ssg/
└── monthly.tsx # Uses SSG (inner folder wins)
Important notes:
dashboard+ssr/analytics becomes /dashboard/analyticsAPI routes benefit too:
With folder suffixes, API routes no longer require the +api.tsx file suffix:
Terminal
app/
└── api+api/ # Folder suffix
├── users.tsx # No +api.tsx needed! → /api/users (API)
├── posts.tsx # No +api.tsx needed! → /api/posts (API)
└── auth/
└── login.tsx # Inherits API type → /api/auth/login (API)
This makes organizing API routes much cleaner, especially for versioned APIs like v1+api/ and v2+api/.
Note that render modes mostly only apply to the web. On native, everything is essentially a SPA, with all the JS for every route included in the app the user downloads from the app store. Because native apps don’t have to deal with the network, this SPA mode is ideal and has very little downsides.
When you don’t add a + suffix to your route, it will default to an “SSG” route, unless you set the web.defaultRenderMode configuration otherwise. An example of a SSG route would be something like app/blog.tsx if you haven’t changed the defaultRenderMode, or if you have app/blog+ssg.tsx.
SSG Stands for “Server Side Generated”, it’s a strategy for web where you pre-render the entire HTML and CSS of a page at build-time fully on the server (or in CI) so that you can serve the page quickly from something like a CDN, without running any JavaScript at the time of request.
We chose SSG as the default behavior as it serves as a good balance between simplicity and performance. What it is less good at is serving dynamic content to an individual user.
That said - you can still do dynamic content on top of an SSG page. It’s just that you’ll have to have the dynamic content load in after the browser finishes downloading and parsing the JS for that page, and it will need to gracefully replace the static content that the user sees.
This pattern is great for things like a SaaS homepage that shows mostly static content, but where you may want to load a logged-in menu for the current user. It’s not good for something like a logged-in dashboard.
If you name a route with the +spa suffix, like app/dashboard+spa.tsx, it will no longer render on the server - either at build-time or at request. Instead, at build time One will build just the client-side JavaScript necessary.
This render mode is great for highly dynamic use cases. Think a Linear, or a Figma, a metrics dashboard, or a user account panel. It is simpler to build, and doesn’t require making sure every dependency works on both Node.js and in the browser.
It’s downsides include a slower initial load time, and worse SEO. You can mitigate this with the renderRootLayout option, which renders your root layout on the server while keeping page content client-rendered.
If you name a route with the +ssr suffix, like app/issues+ssr.tsx, you will get a server rendered page. This mode will generate the JS for both server and client at build-time, but instead of rendering out the HTML and CSS right then, it will instead wait for a request to the production server before it imports the server JS, renders the page, and then returns the HTML and CSS.
This mode is great for things that need to be dynamically rendered more often than whenever you deploy. One example is something more like GitHub Issues. By server rendering a page like this, you get faster initial loads, and better SEO than a SPA-type page. But it is the most expensive in terms of cost - each request will now run a full render of the route, and also slower than SSG in terms of initial response time, at least by default.
In the case of a GitHub Issues type page, what you’d do is cache the SSR response on a CDN, and then clear the CDN cache whenever data for that page updates. This is complex, and generally SSR is the most complex of the three render modes to support. Because it is more complex and more expensive, we generally recommend using SPA or SSG unless you are certain you can afford to pay for the extra render cost, and/or cache responses without too much trouble.
Adding +api.ts to the end of your filename turns a route into an API route. API routes are HTTP endpoints that you call from other routes or services, not pages that users navigate to directly.
app/api/hello+api.ts
export function GET(request: Request) {
return Response.json({ message: 'Hello!' })
}
API routes use Web standard Request and Response objects.
See API Routes for full documentation.
Edit this page on GitHub.