Next.js: Build error when slug ends with `.html` for dynamic routes

Created on 26 Apr 2020  Â·  9Comments  Â·  Source: vercel/next.js

Bug report

I have an old site which I'm about to migrate to Next.js. I want to generate static pages. I have to support URLs such as: /blog/article-slug.html (this is the canonical URL that users and search engines see). I found two ways to build such URLs but neither of them seem to work properly.

Describe the bug

Option 1

I create a page component such as pages/blog/[slug].html.js and in getStaticPaths I provide the slug without the .html extension.

export const getStaticPaths = async () => {
    return { fallback: false, paths: [{ params: { slug: 'article-slug' } }] };
};

This options does not work at all. I DEV mode I only get a 404 page if I try to access http://localhost:3000/blog/article-slug.html. When running next build I get

Error: getStaticPaths can only be used with dynamic pages, not '/articles/[slug].html'.
This means that this page is considered to have a static path and not be a dynamic.

Option 2

I create a page component such as pages/blog/[slug].js and in getStaticPaths I provide the slug with the .html extension.

export const getStaticPaths = async () => {
    return { fallback: false, paths: [{ params: { slug: 'article-slug.html' } }] };
};

This option at least works in DEV mode. But when running next build I get the following error:

> Build error occurred
[Error: ENOENT: no such file or directory, rename '/tmp/next/.next/export/blog/article-slug.html.html' -> '/tmp/next/.next/server/static/78zmPfx5_aE06CmpsBKfD/pages/blog/article-slug.html.html'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'rename',
  path: '/tmp/next/.next/export/blog/article-slug.html.html',
  dest: '/tmp/next/.next/server/static/78zmPfx5_aE06CmpsBKfD/pages/blog/article-slug.html.html'
}

To Reproduce

I created a repo to reproduce these errors:
https://github.com/bdadam/next.js-error-when-.html-in-path

Expected behavior

I would expect that next build generates a file such as .next/export/blog/article-slug.html.html.

System information

  • OS: Linux, Ubuntu 19.10
  • Version of Next.js: 9.3.5
  • Version of Node.js: 13.6.0

Other considerations

I can only reproduce this behavior if the slug ends with .html a . itself in the slug does not seem to cause any problems e.g. article....slug.xhtml.

good first issue bug

Most helpful comment

I think this could be solved by the new "rewrite" function, though you'll need a new version of Nextjs. Example:

module.exports = {
    async rewrites() {
        return [
            {
                source: "/:slug*.html",  // Old url with .html
                destination: "/:slug*", // Redirect without .html
            },
        ];
    },   
}

All 9 comments

I investigated the issue a little bit. The code causing the problem seems to be here: https://github.com/zeit/next.js/blob/960c18da53af89115c7e96dc8188646a51c53c15/packages/next/export/worker.js#L109

const pageExt = extname(page)
    const pathExt = extname(path)
    // Make sure page isn't a folder with a dot in the name e.g. `v1.2`
    if (pageExt !== pathExt && pathExt !== '') {
      // If the path has an extension, use that as the filename instead
      htmlFilename = path
    } else if (path === '/') {
      // If the path is the root, just use index.html
      htmlFilename = 'index.html'
    }

In my case above (Option 2) the variable console.log('Path', path, '; Page', page) gives Path /blog/article-slug.html ; Page /blog/[slug] therefore pageExt is empty and pathExt is .html.

This causes htmlFilename to equal to /blog/article-slug.html instead of /blog/article-slug.html.html (please not the double .html). If I manually change this line then https://github.com/zeit/next.js/blob/960c18da53af89115c7e96dc8188646a51c53c15/packages/next/build/index.ts#L808 will not fail anymore because it can find the file with .html.html ending.

In my use-case I would totally be fine with only having a single .html extension on the file. It would only be important than the code generating the file and the code renaming/moving it to the outdir stay consistent.

Currently you can make it work by wrapping Next.js in Express.js, like what you can do in Next.js 8:

const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app
  .prepare()
  .then(() => {
    const server = express()
    server.get('/blog/:slug.html', (req, res) => {
      app.render(req, res, '/blog/${req.params.slug}')
    })

    server.get('*', (req, res) => {
      return handle(req, res)
    })

    server.listen(3000, err => {
      if (err) throw err
      console.log('> Ready on http://localhost:3000')
    })
  })
  .catch(ex => {
    console.error(ex.stack)
  })

However I do wish Next.js have this feature included. I personally prefer the pages/blog/[slug].html.js approach because it is more consistent.

We encountered this bug even when the slug was to be a part of a directory path too:
/pages/[language]/[section]/[slug-with-html]/index.js
Goal URI being: /en/kids/some-article-title.html/
We use an external paid health content API where the article identifier IDs actually have the ".html" in them.
so "some-article-title.html" is the actual API lookup ID.

We encountered this bug even when the slug was to be a part of a directory path too:
/pages/[language]/[section]/[slug-with-html]/index.js
Goal URI being: /en/kids/some-article-title.html/
We use an external paid health content API where the article identifier IDs actually have the ".html" in them.
so "some-article-title.html" is the actual API lookup ID.

Our work-a-round was to use this:
/pages/[language]/[section]/[slug-with-html]/article.js
and then use URL rewriting in the backend so the link included in the content connect.

I got same problem.

Our site migrated to NextJS and historical links contains extension:


Example: /post/NextJSDeepThinking.html

But in nextjs we only have /post/NextJSDeepThinking


Our temp workaround: redirect to routes without extension in 404 Page:

const Custom404 = (): JSX.Element => {
  const router = useRouter();

  useEffect(() => {
    if (router.asPath.includes(".html")) {
       /* if route includes html extension */
      router.push(router.asPath.replace(".html", ""));
    } else {
      router.push("/error");
    }
  }, []);
};

If you access /post/NextJSDeepThinking.html then it will redirect you to /post/NextJSDeepThinking.

In that case then historical links from search engine may work.

But this is only a TEMP solution. It is not user friendly to see Redirecting for all links from search engine. Hope we can get fix from NextJS

I think this could be solved by the new "rewrite" function, though you'll need a new version of Nextjs. Example:

module.exports = {
    async rewrites() {
        return [
            {
                source: "/:slug*.html",  // Old url with .html
                destination: "/:slug*", // Redirect without .html
            },
        ];
    },   
}

I think this could be solved by the new "rewrite" function, though you'll need a new version of Nextjs. Example:

module.exports = {
    async rewrites() {
        return [
            {
                source: "/:slug*.html",  // Old url with .html
                destination: "/:slug*", // Redirect without .html
            },
        ];
    },   
}

Worked for me.

I think this could be solved by the new "rewrite" function, though you'll need a new version of Nextjs. Example:

module.exports = {
    async rewrites() {
        return [
            {
                source: "/:slug*.html",  // Old url with .html
                destination: "/:slug*", // Redirect without .html
            },
        ];
    },   
}

This solution is really elegant and helps a lot, thanks!

I get this error: Error: getStaticPaths can only be used with dynamic pages, not '/blog/shit-world'.

The complete error is:

> Build error occurred
Error: getStaticPaths can only be used with dynamic pages, not '/blog/shit-world'.
Learn more: https://nextjs.org/docs/routing/dynamic-routes
    at Object.isPageStatic (~/node_modules/next/dist/build/utils.js:24:58)
    at execFunction (~/node_modules/jest-worker/build/workers/processChild.js:155:17)
    at execHelper (~/node_modules/jest-worker/build/workers/processChild.js:139:5)
    at execMethod (~/node_modules/jest-worker/build/workers/processChild.js:143:5)
    at process.<anonymous> (~/node_modules/jest-worker/build/workers/processChild.js:64:7)
    at process.emit (events.js:314:20)
    at emit (internal/child_process.js:902:12)
    at processTicksAndRejections (internal/process/task_queues.js:81:21) {
  type: 'Error'
}

And the weird part is I don't even have a slug ending with .html. Can't figure this thing out 🤔

It works with next dev but breaks on next build.

Edit:

No worries, it was my fault. I found the answer here → https://github.com/vercel/next.js/issues/17114#issuecomment-694791816

Turns out, I was creating a blog using a different method earlier & had getStaticPaths left over in my code but it is not needed when I'm directly serving .mdx posts from the pages/ folder. It should only be used while using dynamic routes aka pages/blog/[slug].js

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sospedra picture sospedra  Â·  3Comments

swrdfish picture swrdfish  Â·  3Comments

formula349 picture formula349  Â·  3Comments

kenji4569 picture kenji4569  Â·  3Comments

havefive picture havefive  Â·  3Comments