reactjsnode.jskoapreact

only homepage renders but not other pages


I have a basic fullstack app built with Node, Koa, and Preact. I'm trying to get Preact routing to work. I created a component and route for an About page. When I start the Preact dev server I can access the /about page successfully at localhost:5173/about.

The problem is that when I start my Node server I get Not Found when I try to go to localhost:8080/about. However, I can successfully render the homepage by going to /.

(root) index.js

const server = require('./server');

const port = 8080;

server.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

server\index.js

const Koa = require('koa');
const serve = require('koa-static');
const path = require('path');

const server = new Koa();

// Serve static files
server.use(serve(path.join(__dirname, '../client/dist')));

module.exports = server;

client\src\pages\home\index.jsx

export function Home() {
    return (
        <h1>Preact App Homepage</h1>
    );
}

client\src\pages\about\index.jsx

export function About() {
    return (
        <h1>About Page</h1>
    );
}

client\src\index.jsx

import { LocationProvider, Router, Route, hydrate, prerender as ssr } from 'preact-iso';
import { Header } from './components/Header.jsx';
import { Home } from './pages/Home/index.jsx';
import { About } from './pages/_about.jsx';
import { NotFound } from './pages/_404.jsx';
import './style.css';

export function App() {
    return (
        <LocationProvider>
            <Header />
            <main>
                <Router>
                    <Route path="/" component={Home} />
                    <Route path="/about" component={About} />
                    <Route default component={NotFound} />
                </Router>
            </main>
        </LocationProvider>
    );
}

if (typeof window !== 'undefined') {
    hydrate(<App />, document.getElementById('app'));
}

export async function prerender(data) {
    return await ssr(<App {...data} />);
}

client\dist\index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" >
        <link rel="icon" type="image/svg+xml" href="/vite.svg" >
        <meta name="viewport" content="width=device-width, initial-scale=1.0" >
        <meta name="color-scheme" content="light dark" >
        <title>Vite + Preact</title>
        <script type="module" crossorigin src="/assets/index-7GmgnfZn.js"></script>
        <link rel="modulepreload" crossorigin href="/assets/index-BgxwHnNi.js">
        <link rel="stylesheet" crossorigin href="/assets/index-C_LgOj56.css">
    </head>
    <body>
        <div id="app"><header><nav><a href="/" class="active">Home</a><a href="/404">404</a></nav></header><main><h1>Preact App Homepage</h1></main><script type="isodata"></script></div>
    </body>
</html>

Solution

  • It looks like you might have two separate issues:

    1. Koa returns a 404 Not Found when accessing any page but /
    2. You're not prerendering all the content you wish to be

    The Koa issue is pretty simple: it looks for HTML files in your static output that match the provided URL, and when it cannot find anything, it falls back to a 404. However, this is essentially only a problem as you're missing HTML content. You may want to configure Koa to instead route all traffic to say /404 when HTML cannot be found, or perhaps /, but it's very subjective. Koa provides it's own Not Found response by default which you may want to think about overriding, but it's not the main problem here.

    As for prerendering, the Preact prerenderer in @preact/preset-vite (full disclosure: I wrote it) will crawl your app looking for links to prerender. Essentially, it'll render /, read the resulting HTML, and extract links within (<a href="/about">About Us</a> -> /about) to continue to prerender with, stopping once all pages have been prerendered and no more links have been found. This however can be an issue if you haven't actually linked to a route you want prerendered; typically you'd see this with error pages more than anything, as you're probably not going to link to (say) /404 or /500, but you might want them prerendered all the same.

    In the default prerender template we add links to the header (which you haven't uploaded here) to cover this case, but I'd guess you've removed them, hence why the /about page isn't prerendered.

    To fix this, you have two options:

    1. Ensure all the pages you want prerendered have discoverable links
    2. Use the additionalPrerenderRoutes plugin option to explicitly add pages
    // vite.config.js
    export default defineConfig({
      plugins: [
        preact({
          prerender: {
            enabled: true,
            // ...
            additionalPrerenderRoutes: ['/about', '/error-page', '...']
          }
        })
      ]
    });
    

    Prerender config docs