1-4 Router
In this section, we'll integrate Vue Router to enable SSR based on the URL and client-side routing.
Full Code is available at 4-router
First, create routes in pages directory and add links in App.vue
pages/hello.vue
<script setup lang="ts"></script>
<template>
<h1>Hello,</h1>
</template>
pages/world.vue
<script setup lang="ts"></script>
<template>
<h1>World!</h1>
</template>
App.vue
<script lang="ts" setup>
import { RouterView, RouterLink } from "vue-router";
</script>
<template>
<main>
<RouterView></RouterView>
<RouterLink to="/hello">Go to Hello</RouterLink>
<RouterLink to="/world">Go to World</RouterLink>
</main>
</template>
Next, create a router.ts
We will make feature to automatically make routes from pages directory in the future, but we need to define routes manually for now. In Sever Side, we can not use createWebHistory, so we need switch history based on import.meta.server
.
router.ts
import {
type RouteRecordRaw,
createRouter as _createRouter,
createMemoryHistory,
createWebHistory,
} from "vue-router";
import Hello from "./pages/hello.vue";
import World from "./pages/world.vue";
export const createRouter = () => {
const routes = [
{
path: "/hello",
component: Hello,
},
{
path: "/world",
component: World,
},
] satisfies RouteRecordRaw[];
const history = import.meta.server
? createMemoryHistory()
: createWebHistory();
const router = _createRouter({
history,
routes,
});
return router;
};
Add build config to enable to use import.meta.server
vite.ts
const clientConfig = mergeConfig(defaultConfig, {
build: {
rollupOptions: {
input: join(import.meta.dirname, "entry.client.ts"),
output: {
entryFileNames: "entry.client.js",
},
},
},
define: {
"import.meta.server": false,
},
} satisfies InlineConfig);
const severConfig = mergeConfig(defaultConfig, {
build: {
rollupOptions: {
input: join(import.meta.dirname, "entry.server.ts"),
output: {
entryFileNames: "entry.server.js",
},
},
},
define: {
"import.meta.server": true,
},
} satisfies InlineConfig);
type.d.ts
This is necessary to avoid type error, because import.meta.server
is not defined in default.
interface ImportMeta {
server: boolean;
}
Install router in entry.server.ts and entry.client.ts
entry.server.ts
We need to push initial URL manually in server side. And router.isReady is necessary to avoid hydration error. Will explain about ctx later.
export default async (ctx: { url: string }) => {
const app = createApp(App);
const router = createRouter();
router.push(ctx.url);
await router.isReady();
app.use(router);
return app;
};
entry.client.ts
On client side, the router automatically picks up initial location from the URL, so we don't need to push the URL manually.
const initApp = async () => {
const router = createRouter();
await router.isReady();
const app = createSSRApp(App);
app.use(router);
app.mount("#__nuxt");
};
Pass SSR Context when renderToString is called
The renderToString
function receives the SSR context as the first argument and passes that context to the createApp
function we provided to createRenderer
. (source code) This is the context we received in entry.server.ts
. So let's modify render.ts
to pass the context.
render.ts
export const renderMiddleware = defineEventHandler(async (event) => {
if (!renderer) await setupRenderer();
const { req, res } = event.node;
if (req.url === "/entry.client.js") {
const code = readFileSync(
join(import.meta.dirname, "dist/entry.client.js"),
"utf-8"
);
res.setHeader("Content-Type", "application/javascript");
res.end(code);
}
const rendered = await renderer.renderToString({ url: req.url });
const data = renderHTML(rendered);
res.setHeader("Content-Type", "text/html;charset=UTF-8");
res.end(data, "utf-8");
});
Run the server
If you run the server, you can see client side routing works and server returns HTML based on requested URL.