nuxt-modules / i18n

I18n module for Nuxt
https://i18n.nuxtjs.org
MIT License
1.75k stars 483 forks source link

nuxt generate won't generate index.html with prefix strategy #3016

Open steffenstolze opened 4 months ago

steffenstolze commented 4 months ago

Environment

Reproduction

https://github.com/steffenstolze/nuxt_i18n_bug

Describe the bug

Running nuxt generate should create an index.html file in the .output/public folder when using strategy: 'prefix' in the i18n config. If this file is missing, you get a 404 when accessing root, since every route is prefixed with the locale code.

With the latest Nuxt and Nuxt i18n versions, it doesn't:

"dependencies": {
    "@nuxtjs/i18n": "8.3.1",
    "nuxt": "3.12.3"
}
image

With older ones it works up to these versions:

"dependencies": {
    "@nuxtjs/i18n": "8.3.0",
    "nuxt": "3.11.2"
}
image

From here on, if you bump up @nuxtjs/i18n to 8.3.1 or nuxt to 3.12.0 and higher, the index.html is not created anymore.

Additional context

As a workaround I added the following to the nitro config, to generate the index.html manually once the 200.html file is created.

While this works, it also feels super hacky and shouldn't be needed.

    nitro: {
        static: true,
        // Hacky approach to redirect create index.html file for root redirect
        hooks: {
            'prerender:generate'(route, nitro) {
                if (route?.route === '/200.html') {
                    const redirectHtml =
                        '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=/de"></head></html>';
                    const outputPath = path.join(
                        nitro.options.output.publicDir,
                        'index.html'
                    );
                    writeFileSync(outputPath, redirectHtml);
                }
            },
        },
    },

Logs

No response

Skyost commented 4 months ago

This is very problematic, because accessing the index page of any website that uses Nuxt i18n leads to a 404 error.

Based on your workaround, I've created a little module to automatically redirect the user to its preferred language. Create a modules/i18n-fix-index.ts file with the following content.

import * as fs from 'fs'
import { createResolver, defineNuxtModule, useLogger } from '@nuxt/kit'
import type { PrerenderRoute, Nitro } from 'nitropack'

/**
 * Options for the module.
 *
 * @interface
 */
export interface ModuleOptions {
  /**
   * Accepted languages codes.
   */
  acceptedLanguages: string[]
  /**
   * The cookie name.
   */
  i18nCookieName: string
}

/**
 * The name of the module.
 */
const name = 'i18n-fix-index'

/**
 * The logger instance.
 */
const logger = useLogger(name)

/**
 * TODO: Should be removed once "https://github.com/nuxt-modules/i18n/issues/3016" is fixed.
 */
export default defineNuxtModule<ModuleOptions>({
  meta: {
    name,
    version: '0.0.1',
    compatibility: { nuxt: '^3.0.0' },
    configKey: 'i18nFixIndex'
  },
  defaults: {
    acceptedLanguages: ['en', 'fr'],
    i18nCookieName: 'i18n_redirected'
  },
  setup: async (options, nuxt) => {
    const defaultLanguage = options.acceptedLanguages[0]
    const indexPageContent = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="refresh" content="3; url=/${defaultLanguage}/">
  <script type="text/javascript">
    const getCookieValue = (name) => (
      document.cookie.match('(^|;)\\\\s*' + name + '\\\\s*=\\\\s*([^;]+)')?.pop() || ''
    )

    const acceptedLanguages = ${JSON.stringify(options.acceptedLanguages)}
    const i18nCookieValue = getCookieValue('${options.i18nCookieName}')
    let wantedLanguage = acceptedLanguages[0]
    if (acceptedLanguages.includes(i18nCookieValue)) {
      wantedLanguage = i18nCookieValue;
    } else {
      const userLanguage = navigator.language || navigator.userLanguage
      const languageCode = userLanguage.indexOf('-') === -1 ? userLanguage : userLanguage.split('-')[0]
      if (acceptedLanguages.includes(languageCode)) {
        wantedLanguage = languageCode
      }
    }
    document.querySelector('meta[http-equiv="refresh"]').setAttribute('content', '0; /' + wantedLanguage + '/')
    document.querySelector('p > a').setAttribute('href', '/' + wantedLanguage + '/')
  </script>
</head>
<body>
  <p style="margin-bottom: 0;">
    Redirecting you... Please wait or click <a href="/${defaultLanguage}/">here</a> if you're not being redirected.
  </p>
</body>
</html>`
    const resolver = createResolver(import.meta.url)
    const hooks = nuxt.options.nitro.hooks ?? {}
    // @ts-expect-error: We're not in nuxt.config.ts.
    hooks['prerender:generate'] = (route: PrerenderRoute, nitro: Nitro) => {
      if (route?.route === '/200.html') {
        logger.info('Fixing index...')
        const outputPath = resolver.resolve(
          nitro.options.output.publicDir,
          'index.html'
        )
        fs.writeFileSync(outputPath, indexPageContent)
        logger.success('Done. Don\'t forget to delete this module once https://github.com/nuxt-modules/i18n/issues/3016 is closed.')
      }
    }
    nuxt.options.nitro.hooks = hooks
  }
})

Then, in your nuxt.config.ts, you have to configure it.

i18nFixIndex: {
  acceptedLanguages: ['en', 'fr'], // Put the accepted languages codes. The first of the list will be the default language.
  i18nCookieName: 'i18n_redirected' // `cookieKey` (https://i18n.nuxtjs.org/docs/options/browser#cookiekey) of Nuxt i18n.
}
JNietou commented 2 months ago

Confirmed, I have tried to change the versions of each dependency and the exact same thing happens to me.

codeflorist commented 1 month ago

combined with #3062 means prefix strategy is pretty broken atm.

skarnl commented 1 month ago

Ran into this today. After puzzling for quiet a while, I (think) I fixed it with the following changes:

  1. change the strategy back to prefix_and_default:
// nuxt.config.ts
i18n: {
    strategy: 'prefix_and_default',

This will cause the index.html to be generated in the root.

But now we miss the defaultLocale in the url ... so to fix this, I added a simple middleware:

In the /middleware folder (create it, if it doesn't exist), create a file ... call it: redirect-root.global.ts (or .js). The name doesn't really matter, except the .global.-part ... that's important!

The content of this middleware:

export default defineNuxtRouteMiddleware((to, from) => {
  const { $i18n } = useNuxtApp();

  if (to.path === '/') {
    const defaultLocale = $i18n.defaultLocale || $i18n.fallbackLocale || 'en';
    return navigateTo(`/${defaultLocale}`);
  }
});

Now we will be redirected from the / to the defaultLocale 👍

JNietou commented 1 month ago

Ran into this today. After puzzling for quiet a while, I (think) I fixed it with the following changes:

  1. change the strategy back to prefix_and_default:
// nuxt.config.ts
i18n: {
    strategy: 'prefix_and_default',

This will cause the index.html to be generated in the root.

But now we miss the defaultLocale in the url ... so to fix this, I added a simple middleware:

In the /middleware folder (create it, if it doesn't exist), create a file ... call it: redirect-root.global.ts (or .js). The name doesn't really matter, except the .global.-part ... that's important!

The content of this middleware:

export default defineNuxtRouteMiddleware((to, from) => {
  const { $i18n } = useNuxtApp();

  if (to.path === '/') {
    const defaultLocale = $i18n.defaultLocale || $i18n.fallbackLocale || 'en';
    return navigateTo(`/${defaultLocale}`);
  }
});

Now we will be redirected from the / to the defaultLocale 👍

thank u! I'll update to latest version and try your fix. Looks good and simple :)

JNietou commented 1 month ago

Sadly this solution from @skarnl doesn't work (with SSG) to me because by not landing in index.html the middleware doesn't do its job. In the end I created a script that adds in index.html with the old school trick to redirect: <meta http-equiv="refresh" content="0; url=/en" />. Another solution could be to copy the index.html of the main language also in the root (npm run generate && cp .output/public/en/index.html .output/public/index.html )

JNietou commented 1 month ago

I have also detected that the problem is more serious than it seems, there are pages that I generate from CMS that are generated without problems, but the local pages are not, none at all. There is no index for these pages.

gbendjaf commented 3 weeks ago

+1 I'm eager to have update on this matter as it is problematic to not be able to statically render any nuxt site using @nuxt/i18n

Downgrading versions to "dependencies": { "@nuxtjs/i18n": "8.3.0", "nuxt": "3.11.2" } does not do the trick on vercel with nuxt build --prerenderr or nuxt generate with static: true (cf: https://nuxt.com/blog/going-full-static#current-issues)

If anyone got it working by downgrading versions I would like to know wich ones you used :)

steffenstolze commented 3 weeks ago

I still use the same "hack" from my original post where I create a nitro hook:

nitro: {
        static: true,
        // Hacky approach to redirect create index.html file for root redirect
        hooks: {
            'prerender:generate'(route, nitro) {
                if (route?.route === '/200.html') {
                    const redirectHtml =
                        '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=/de"></head></html>';
                    const outputPath = path.join(
                        nitro.options.output.publicDir,
                        'index.html'
                    );
                    writeFileSync(outputPath, redirectHtml);
                }
            },
        },
    },

it still works flawlessly.

gbendjaf commented 3 weeks ago

writeFileSync

How do you import writeFileSync in your config ?

JNietou commented 2 weeks ago

+1 I'm eager to have update on this matter as it is problematic to not be able to statically render any nuxt site using @nuxt/i18n

Downgrading versions to "dependencies": { "@nuxtjs/i18n": "8.3.0", "nuxt": "3.11.2" } does not do the trick on vercel with nuxt build --prerenderr or nuxt generate with static: true (cf: https://nuxt.com/blog/going-full-static#current-issues)

If anyone got it working by downgrading versions I would like to know wich ones you used :)

The answer is in the first post.

gbendjaf commented 2 weeks ago

Ok, so I got it working but slightly differently from the version in the first post :

import fs from 'node:fs'
import path from 'node:path'

export default defineNuxtConfig({
  target: 'static',

  nitro: {
        hooks: {
            'prerender:generate'(route, nitro) {
                if (route?.route === '/200.html') {
                    const redirectHtml =
                        '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=/fr"></head></html>';
                    const outputPath = path.join(
                        nitro.options.output.publicDir,
                        'index.html'
                    );
                    fs.writeFileSync(outputPath, redirectHtml);
                }
            },
        },
    },
})

So the 3 differences are :

Hope it can help someone :)