facebook / docusaurus

Easy to maintain open source documentation websites.
https://docusaurus.io
MIT License
56.63k stars 8.51k forks source link

Prevent initial 404 for dynamic routes #7816

Closed raedle closed 2 years ago

raedle commented 2 years ago

Have you read the Contributing Guidelines on issues?

Prerequisites

Description

I wrote a custom plugin plugin-dynamic-routes, which adds non-exact routes to the Docusaurus routes configuration.

The plugin-dynamic-routes plugin:

module.exports = function (context, options) {
  const { siteConfig } = context;
  return {
    name: "plugin-dynamic-routes",

    async contentLoaded({ content, actions }) {
      const { routes } = options;
      const { addRoute } = actions;
      routes.map((route) => {
        // Adjust route to include base url
        const updatedRoute = {
          ...route,
          path: path.join(siteConfig.baseUrl, route.path)
        };
        addRoute(updatedRoute);
      });
    }
  };
};

Example use in docusaurus.config.js:

plugins: [
  [
    path.resolve(__dirname, "plugin-dynamic-routes"),
    {
      routes: [
        {
          path: "/example",
          exact: false,
          component: "@site/src/components/ExampleRouter"
        }
      ]
    }
  ]
],

The ExampleRouter component uses react-router-dom to enable custom routes with a Switch:

export default function ExampleRouter({ match }) {
  // match.url should be {baseUrl}/example
  return (
    <Switch>
      <Route path={`${match.url}/:slug`} component={Test} />
    </Switch>
  );
}

The enables dynamic routes for a static Docusaurus deployment. The configuration will render the ExampleRouter component, which itself uses the React Router for custom route rendering.

For example, the url "https://example.com/example/foo" will render the Test component with a route param slug="foo".

It works as you can see in the screenshot, but for a brief moment before the custom route takes over, it renders a "Page Not Found" error because example/foo returns a 404 (see screencast at (0:33 min).

https://user-images.githubusercontent.com/489051/180080372-74d458e6-1065-4d3d-a76f-8ed8992f8eb6.mp4

Note: The site has to be built with yarn build for the issue to show. The development version does not have the 404.

I am aware that this is not something typically supported with a static Docusaurus deployment, but was wondering if there is a workaround to prevent the 404 and "Page Not Found" or maybe render a page saying that client side routing is taking over.

Thanks in advance!

Reproducible demo

https://codesandbox.io/s/docusaurus-dynamic-route-ktgqcb

Steps to reproduce

  1. Go to https://codesandbox.io/s/docusaurus-dynamic-route-ktgqcb
  2. Open new terminal
  3. Run yarn build && yarn serve
  4. Accept to start server on new port
  5. Accept to open in browser to view build
  6. Extend the browser url with example/foo
  7. Notice the "Page Not Found" before dynamic route kicks in

Expected behavior

The expected behavior would be that the dynamic route renders the routed component with its content

Actual behavior

The actual behavior is that the dynamic route first shows the "Page Not Found" due to a 404 and then renders the routed component with its content

Your environment

Self-service

Josh-Cena commented 2 years ago

Can you try creating a NotFound theme component, check if the current route matches your dynamic route, and if so, return an empty Layout with a <Loading>? (We have a theme component called Loading)

raedle commented 2 years ago

Thanks for addressing my question, @Josh-Cena.

I swizzled the NotFound component and checked for the pathname in the location. It works without the plugin-dynamic-routes for the registered routes. However, when the plugin with dynamic routes are enabled, it keep falling back to the default NotFound. I tried swizzle eject and swizzle wrap.

The website repo is in the PlayTorch repo.

Any ideas what I'm doing wrong?

Swizzled code (ejected):

import React from 'react';
import Translate, {translate} from '@docusaurus/Translate';
import {PageMetadata} from '@docusaurus/theme-common';
import Layout from '@theme/Layout';
import {useLocation} from '@docusaurus/router';

export default function NotFound() {
  const location = useLocation();

  console.log(location);

  if (location.pathname.startsWith('/foo')) {
    return (
      <>
        <PageMetadata
          title={translate({
            id: 'theme.NotFound.title',
            message: 'Page Not Found',
          })}
        />
        <div>Foo Route</div>
      </>
    );
  } else {
    if (location.pathname.startsWith('/snack')) {
      return (
        <>
          <PageMetadata
            title={translate({
              id: 'theme.NotFound.title',
              message: 'Page Not Found',
            })}
          />
          <div>Snack Route</div>
        </>
      );
    }
  }

  return (
    <>
      <PageMetadata
        title={translate({
          id: 'theme.NotFound.title',
          message: 'Page Not Found',
        })}
      />
      <Layout>
        <main className="container margin-vert--xl">
          <div className="row">
            <div className="col col--6 col--offset-3">
              <h1 className="hero__title">
                <Translate
                  id="theme.NotFound.title"
                  description="The title of the 404 page">
                  Page Not Found 123
                </Translate>
              </h1>
              <p>
                <Translate
                  id="theme.NotFound.p1"
                  description="The first paragraph of the 404 page">
                  We could not find what you were looking for.
                </Translate>
              </p>
              <p>
                <Translate
                  id="theme.NotFound.p2"
                  description="The 2nd paragraph of the 404 page">
                  Please contact the owner of the site that linked you to the
                  original URL and let them know their link is broken.
                </Translate>
              </p>
            </div>
          </div>
        </main>
      </Layout>
    </>
  );
}
slorber commented 2 years ago

@raedle at the end of the day, you must configure the hosting solution to serve a page for your URLs that is not the 404 page.

Swizzling the NotFound component is only a workaround because you also want the host to answer status 200 and not status 404

Docusaurus can't handle that with just static files, you have to handle it yourself with the proprietary host config.

We do this when integrating the Canny feedback widget in Docusaurus:


Note, some frameworks like Gatsby will have a generic API like createRedirect({from,to}) and plugins like gatsby-plugin-netlify that will generate the appropriate proprietary _redirects file for you. We decided to not provide such abstraction. In my experience it's not worth it and often there are some slightly different behaviors for each hosts and the abstraction does not really make your code 100% portable. For example, we use the :splat syntax of Netlify (Cloudflare also has something similar), which is quite different from the syntax offered on Vercel.

For now, we don't have a way to be able to create dynamic routes in Docusaurus with a plugin: in any case, you'll have to tweak your host config.

raedle commented 2 years ago

Thanks @slorber for the detailed response, and for providing action items for us to enable dynamic routes. We will be considering a different hosting platform (e.g., Vercel).

Closing the task, and thanks again!