vite-pwa / vite-plugin-pwa

Zero-config PWA for Vite
https://vite-pwa-org.netlify.app/
MIT License
3.12k stars 204 forks source link

Nuxt3. When enter page ssr returns me fallback page but then on hydration it renders me normal page #553

Open Dimassss opened 1 year ago

Dimassss commented 1 year ago

Problem: For example I have page /hello1 and /hello2. Also I has fallback page /offline. /hello1 is prerendered on build step and /hello2 doesnt. As a result, when I enter on /hello1 page it renders as it must be, but /hello2 (when it was done on server side, for example after f5 on that page) firstly shows up as /offline and then immediately renders as it must be on the client side. Of course, such behaviour exists only when vite pwa is connected. This is my nuxt.config.ts file. If you need something else ask me.

What I want as a solution: There must be no fallback page if page exists. If page /hello2 exists but is not prerendered (so is nit precached by workbox), so there must be done normaly request to server and rendered html which was rendered on ssr.

import path from 'path';
import { IPXOptions } from 'ipx';
import i18nConfig, { domainsLanguageMap } from './config/i18n'
import buildSitemaps from './config/sitemap';
import { RobotsTXT } from './lib/modules/robots.txt';
import questions from './data/pages/pomoc--page/questions.uk.json'

const env = {
  apiVersion: process.env.API_VERSION || 'v1',
  searchEAUrl: process.env.SEARCH_EA_URL,
  hostUrl: process.env.HOST_URL,
  domain: process.env.DOMAIN,
  EAHostUA: process.env.EA_HOST_UA,
  cdnLink: process.env.CDN_LINK,
  apiUrl: process.env.API_URL,
  lang: (domainsLanguageMap[process.env.DOMAIN] ?? 'pl') as 'pl' | 'uk',
  mockServerResponseOnError: (process.env.MOCK_SERVER_RESPONSE_ON_ERROR ?? '').toUpperCase() === 'TRUE',
  strapiAuthorizationToken: process.env.STRAPI_AUTHORIZATION_TOKEN,
  strapiUrl: process.env.STRAPI_URL,
  strapiPublicationState: process.env.STRAPI_PUBLICATION_STATE as 'live' | 'preview' | undefined,
  environment: process.env.ENVIRONMENT ?? 'local'
}

const i18n = i18nConfig(env.lang, env.environment)
const i18nTranslations = i18n.vueI18n.messages[env.lang]

export default defineNuxtConfig({
  ssr: true,
  pages: true,
  typescript: {
    strict: true,
    typeCheck: true
  },
  runtimeConfig: {
    apiUrl: env.apiUrl,
    mockServerResponseOnError: env.mockServerResponseOnError,
    public: {
      apiVersion: env.apiVersion,
      hostUrl: env.hostUrl,
      domain: env.domain,
      cdnLink: env.cdnLink
    }
  },
  app: {
    head: {
      title: i18nTranslations.commons.website.name,
      htmlAttrs: {
        lang: env.lang
      },
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { name: 'description', content: i18nTranslations.commons.website.name },
        { name: 'format-detection', content: 'telephone=no' }
      ],
      link: [
        {
          rel: 'icon',
          type: 'image/x-icon',
          href: '/img/autokar-icon.png'
        }
      ],
      script: [
        { src: `/analytics/gtm.js?id=${process.env.GTM_ID}` },
        { src: `/analytics/hotjar.js?id=${process.env.HJ_ID}&sv=${process.env.HJ_SV}` }
      ],
      bodyAttrs: {
        class: 'main'
      }
    }
  },
  modules: [
    '@pinia/nuxt',
    ['@nuxtjs/i18n', i18n],
    ['@nuxt/image', {
      provider: 'ipx',
      ipx: {
        dir: 'public',
        baseURL: new URL('/_ipx', process.env.CDN_LINK),
        modifiers: {
          background: '#00000000',
          format: 'webp',
          fit: 'contain'
        }
      }
    }],
    '@vite-pwa/nuxt'
  ],
  pwa: {
    registerType: 'autoUpdate',
    manifest: {
      name: i18nTranslations.commons.website.name,
      short_name: i18nTranslations.commons.website.name,
      start_url: '/',
      display: 'fullscreen',
      theme_color: '#ffffff',
      prefer_related_applications: false,
      icons: [16, 16, 192, 192, 512, 512].map((s, i) => ({
        src: `ico/img.ico.${s}.${i % 2 ? 'maskable' : 'normal'}.png`,
        sizes: `${s}x${s}`,
        type: 'image/png',
        purpose: i % 2 ? 'maskable' : undefined
      })),
      categories: ['transportation', 'travel'],
      description: i18nTranslations.manifest.description,
      shortcuts: i18nTranslations.manifest.shortcuts,
      orientation: 'portrait-primary'
    },
    workbox: {
      maximumFileSizeToCacheInBytes: 3 * (1024 ** 2),
      navigateFallback: '/offline',
      globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
      runtimeCaching: [
        {
          urlPattern: /^http(s)?:\/\/(front\.ea2|(dev\.|rc\.)?domain\.com\.ua)\/(?!(api\/)?(pl|uk)\/v1\/)/g,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'app',
            expiration: {
              maxEntries: 1000,
              maxAgeSeconds: 60 * 60 * 24 * 365 // <== 365 days
            },
            cacheableResponse: {
              statuses: [0, 200]
            }
          }
        },
        {
          // eslint-disable-next-line no-useless-escape
          urlPattern: /^http(s)?:\/\/(front\.ea2|(dev\.|rc\.)?domain\.com\.ua)\/api\/(pl|uk)\/v1\/(main\/[\-a-zA-Z]+|wp\/[a-z]+|config)$/g,
          handler: 'StaleWhileRevalidate',
          options: {
            cacheName: 'api',
            expiration: {
              maxEntries: 1000,
              maxAgeSeconds: 60 * 60 * 24 * 365 // <== 365 days
            },
            cacheableResponse: {
              statuses: [0, 200]
            }
          }
        }
      ],
      navigateFallbackDenylist: [
        /^http(s)?:\/\/(front\.ea2|(dev\.|rc\.)?domain\.com\.ua)\/(api\/)?(pl|uk)\/v1\//g,
        /^http(s)?:\/\/(front\.ea2|(dev\.|rc\.)?domain\.com\.ua)\/tickets\//g,
        /^http(s)?:\/\/(front\.ea2|(dev\.|rc\.)?domain\.com\.ua)\/.+\.[a-z\d]+$/g
      ],
      cleanupOutdatedCaches: true,
      navigationPreload: false
    },
    client: {
      installPrompt: true,
      // you don't need to include this: only for testing purposes
      // if enabling periodic sync for update use 1 hour or so (periodicSyncForUpdates: 3600)
      periodicSyncForUpdates: 3600
    },
    devOptions: {
      enabled: true,
      navigateFallbackAllowlist: [/^\/$/],
      type: 'module'
    }
  },
  components: [
    {
      path: '~/components/',
      extensions: ['.vue'],
      pathPrefix: true
    },
    {
      path: '~/components/specific/error',
      extensions: ['.vue'],
      pathPrefix: true,
      prefix: 'SpecificError',
      global: true
    },
    {
      path: '~/components/specific/pages/terms--page/blocks/terms-blocks',
      extensions: ['.vue'],
      pathPrefix: true,
      prefix: 'SpecificPagesTermsPageBlocksTermsBlocks',
      global: true
    }
  ],
  css: [
    '@/assets/styles/scss/common/font.scss',
    '@/assets/styles/scss/common/global.scss',
    '@/assets/styles/scss/main.scss'
  ],
  plugins: [],
  postcss: {
    plugins: {
      tailwindcss: {},
      autoprefixer: {}
    }
  },
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "~/assets/styles/scss/variables/index.scss"; @import "v-calendar/dist/style.css";'
        }
      }
    },
    server: {
      hmr: {
        protocol: 'ws',
        host: 'localhost'
      }
    },
    clearScreen: false
  },
  nitro: {
    routeRules: {
      '/assets/**': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/images/**': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/_nuxt/**': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/_ipx/**': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/**/*.js': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/**/*.css': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/**/*.json': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/**/*.html': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/**/*.xml': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/**/*.svg': { headers: { 'Cache-Control': 'max-age=31536000' } },
      '/**/*.png': { headers: { 'Cache-Control': 'max-age=31536000' } }
    },
    compressPublicAssets: true,
    prerender: {
      routes: ((lang: 'pl' | 'uk') => [
        '/',
        '/offline',
        { pl: '/kontakt', uk: '/контакт' }[lang],
        { pl: '/regulamin/1.1', uk: '/основні-положення/1.1' }[lang],
        { pl: '/polityka-prywatnosci', uk: '/політика-конфідеційності' }[lang],
        ...[{ pl: '/pomoc', uk: '/довідка' }[lang]].concat(...questions.map((q: { questions: any[] }, i) => q.questions.map((q, j) => `/${{ pl: 'pomoc', uk: 'довідка' }[lang]}/${i}/${j}`)))
      ])(env.lang)
    }
  },
  hooks: {
    'modules:before': async () => {
      await buildSitemaps(env.lang as 'pl' | 'uk', env.hostUrl, env.apiUrl, env.apiVersion)

      const r = new RobotsTXT()
      r.content = 'User-agent: *\n' +
        'Disallow: \n\n' +
        `Sitemap: ${new URL('sitemap.xml', env.hostUrl)}`;
      await r.writeToFile(path.join(__dirname, './public'))
    }
  }
});
userquin commented 1 year ago

Try excluding all dynamic pages from sw interception (workbox.navigateFallbackDenylist), dont use that cache for html pages, should be with no cache and must revalidate