Redirect based on header OR cookie in Next.js next.config.js - next.js

We're using Next.js and want to route all paths (not just root) to locale-based paths based on the browser Accept-Language header. However, if the user SETS their region, we will set a cookie that would need to be checked first to respect user preferences.
So we need to check for the cookie, and if it's not there, try redirect based on browser language header instead. We're using ISG so limited to next.config.js redirects serverside.
According to the docs, this should work, but since we're using ISG, we need to do this in next.config.js redirects function.
We've tried this solution and it does not work (we get infinite redirects as both cookie AND header match):
const { i18n } = require('./next-i18next.config');
const withTM = require('next-transpile-modules')(['fitty', 'react-svg']); // pass the modules you would like to see transpiled
const handleLocaleRedirects = (path) => {
const result = [];
i18n.locales.forEach((locale) => {
i18n.locales.forEach((loc) => {
if (loc !== locale) {
result.push({
source: `/${locale}${path}`,
has: [
{
type: 'header',
key: 'accept-language',
value: `^${loc}(.*)`,
},
],
permanent: false,
locale: false,
destination: `/${loc}${path}`,
});
result.push({
source: `/${locale}${path}`,
has: [
{
type: 'cookie',
key: 'NEXT_LOCALE',
value: loc,
},
],
permanent: true,
locale: false,
destination: `/${loc}${path}`,
});
}
});
});
return result;
};
module.exports = withTM({
i18n,
reactStrictMode: true,
images: {
domains: [
'dxjnh2froe2ec.cloudfront.net',
'starsona-stb-usea1.s3.amazonaws.com',
],
},
eslint: {
// Warning: Dangerously allow production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
async redirects() {
return [...handleLocaleRedirects('/:celebrityId')];
},
});

I've managed to achieve this using _app.js
Add getInitialProps inside _app.js
It checks cookie inside request, gets current locale using ctx.locale,
My default locale is en-IN so if targetLocale matches default locale it sets an empty string to targetLocale, then redirects using header.
Other than that we don't have to use localeDetection because we are handling on our own.
MyApp.getInitialProps = async ({ ctx }) => {
if (ctx.req) {
const rawCookies = ctx.req.headers.cookie
let locale = ctx.locale
const path = ctx.asPath
if (rawCookies != undefined) {
const cookies = cookie.parse(rawCookies)
let targetLocale = cookies['NEXT_LOCALE']
if (targetLocale != locale) {
if (targetLocale == 'en-IN') {
targetLocale = ''
} else {
targetLocale = '/' + targetLocale
}
ctx.res.writeHead(302, {
Location: `${targetLocale}${path}`
})
ctx.res.end()
}
}
}
return {}
}
Other than this, I'm showing modal when there is no cookie named NEXT_LOCALE to handle first time users.

Related

nextjs dynamic routes doesn't work with next-i18next

I just added next-i18next in my nextjs project following the official guide and everything seemed to be in order, but when I change the language from default (Italian) to English and I go to the detail of an entity then I get 404. This happens only with the dynamic routes and only with the language that is not the default one.
I am going to show you more details.
My next-i18next.config.js file:
module.exports = {
i18n: {
defaultLocale: "it",
locales: ["it", "en"],
},
};
[id].tsx
//My NextPage component...
export async function getStaticPaths() {
const paths = await projects.find()?.map((_project) => ({
params: { id: _project.id + "" },
}));
return {
paths,
fallback: false,
};
}
export async function getStaticProps({
locale,
...rest
}: {
locale: string;
params: { id: string };
}) {
const project = await projects.findOne({id: rest.params.id})
const seo: Seo = {
//...
};
//This row is not the problem, since it persists even if I remove it. Also I am sure that the project exists.
if (!project?.id) return { notFound: true };
return {
props: {
...(await serverSideTranslations(locale, [
"common",
"footer",
"projects",
])),
seo,
project,
},
};
}
index.tsx (under projects folder)
const Projects: NextPage<Props> = ({ /*...*/ }) => {
//...
const router = useRouter();
return <button onClick={() =>
router.push({
pathname: `/projects/[slug]`,
query: { slug: project.slug },
})
}>Read more</button>
}
Also I get the error Error: The provided 'href' (/projects/[slug]) value is missing query values (slug) to be interpolated properly. when I try to change the language while I am in the detail of the project with the italian language set, but I think I did it right according to this doc. As I said before, instead, if I try to go into the dynamic route after having changed the language to "en" then I go to 404 page.
Do you have any suggestions to solve this problem?
I solved this by updating the mothod getStaticPaths to:
export async function getStaticPaths({ locales }: { locales: string[] }) {
const projects = getProjects({ locale: "it" });
const paths = projects.flatMap((_project) => {
return locales.map((locale) => {
return {
params: {
type: _project.slug,
slug: _project.slug,
},
locale: locale,
};
});
});
return {
paths,
fallback: true,
};
}
So there must be passed the locale into the paths.

getStatic Path not working for base URL "/" in NextJS

I'm using Prismic and NextJS for the first time.
What I'm trying to is make it so when the user goes to the base url for the page localhost:3000/ in dev something will load. /About and /Pricing are working fine the base url doesn't work.
import { GetStaticPaths, GetStaticProps } from 'next'
import { SliceZone } from '#prismicio/react'
import * as prismicH from "#prismicio/helpers";
import { createClient, linkResolver } from '../../prismicio'
import { components } from '../../slices'
interface HomePageProps {
page: any
}
const HomePage = (props:HomePageProps) => {
return <SliceZone slices={props.page.data.slices} components={components} />
}
export default HomePage
interface HomePageStaticProps {
params: any
previewData:any
}
export async function getStaticProps(props:HomePageStaticProps) {
console.log("DOES NOT FIRE FOR localhost:3000")
const client = createClient({ previewData:props.previewData })
const params = props.params;
const uid = params?.pagePath?.[params.pagePath.length - 1] || "home";
const page = await client.getByUID("page", uid);
return {
props: {
page,
},
}
}
export const getStaticPaths: GetStaticPaths = async () => {
const client = createClient();
const pages = await client.getAllByType("page");
const paths = pages.map((page) => prismicH.asLink(page, linkResolver)) as string[];
console.log(paths) // [ '/pricing', '/about', '/' ]
return {
paths,
fallback: false,
};
}
or to simplify it further
[[...pagePath]].tsx fails when going to localhost:3000/ but does not fail on localhost:3000/about or localhost:3000/pricing.
import { GetStaticPaths, GetStaticProps } from 'next'
interface HomePageProps {
page: string
}
const HomePage = (props:HomePageProps) => {
return <>{props.page}</>
}
export default HomePage
interface HomePageStaticProps {
params: any
previewData:any
}
export async function getStaticProps(props:HomePageStaticProps) {
const params = props.params;
const uid = params?.pagePath?.[params.pagePath.length - 1] || "home";
//const page = await client.getByUID("page", uid);
return {
props: {
page:uid,
},
}
}
export const getStaticPaths: GetStaticPaths = async () => {
const paths = [ '/pricing', '/about', '/' ];
return {
paths,
fallback: false,
};
}
As far as I can see your'e not fetching the right way. In order to to have a clean project I would recommend to use a const var where you can determine between dev and production enviorenment. To do so you can simply create a file for example: constants.js containing the following:
export const baseurl = process.env.NODE_ENV === "production"
? process.env.NEXT_PUBLIC_DOMAIN // <-- your domain on live
: "http://localhost:3000"; // localhost on dev
Now with this you automatically have localhost on your dev. Notice that you need http:// which your'e missing at the moment. Now the next example shows you how to fetch something on your root / by entering the following code:
import { baseurl } from "../utils/constants"; // <-- importing the constant
// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries.
export async function getStaticProps() {
// Call an external API endpoint to get posts.
// You can use any data fetching library
const res = await fetch(`${baseurl}/api/posts`)
const posts = await res.json()
// By returning { props: { posts } }, the Blog component
// will receive `posts` as a prop at build time
return {
props: {
posts,
},
}
}
If you are using Create-T3-App
https://github.com/t3-oss/create-t3-app
then
your next.config.mjs will default to this as of Nov 7, 2022
const config = {
reactStrictMode: true,
swcMinify: true,
i18n: {
locales: ["en"],
defaultLocale: "en",
},
};
export default config;
remove
const config = {
reactStrictMode: true,
swcMinify: true,
//i18n: {
// locales: ["en"],
// defaultLocale: "en",
//},
};
export default config;
This will make default "/" pathing work, if you require i18n, I'm sorry I can't help.

NextJS rewrites with wildcard query string params

I am having a bit of an issue: I am trying to setup a rewrite in NextJS that will automatically send on the query string to the destination. However, the only examples I can find are with named params. In this case there could be any number of params, so I need a way of making a wildcard (I assume this will be possible?)
What I am looking to do is the following:
/results?param1=a&param2=b... => https://www.somedomain.com/results?param1=a&param2=b...
or
/results?house=red&car=blue&money=none => https://www.somedomain.com/results??house=red&car=blue&money=none
rewrites() {
return [
{
source:'/results?:params*',
destination:'https://www.somedomain.com/results?:params*'
},
Of course this doesn't work, so I looked into has but I cannot workout how to make it work without names params
{
source: '/some-page',
destination: '/somewhere-else',
has: [{ type: 'query', key: 'overrideMe' }],
},
You can use a standard page like this below. What I did is when the router is initialized in a useEffect hook it gets the query (object) and then use the objectToQueryString function to turn it back into a url encoded string, then used the router to redirect to the other page
// /pages/some-page.jsx
import { useRouter } from 'next/router'
import { useEffect } from 'react'
const objectToQueryString = (initialObj) => {
const reducer = (obj, parentPrefix = null) => (prev, key) => {
const val = obj[key];
key = encodeURIComponent(key);
const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key;
if (val == null || typeof val === 'function') {
prev.push(`${prefix}=`);
return prev;
}
if (['number', 'boolean', 'string'].includes(typeof val)) {
prev.push(`${prefix}=${encodeURIComponent(val)}`);
return prev;
}
prev.push(Object.keys(val).reduce(reducer(val, prefix), []).join('&'));
return prev;
};
return Object.keys(initialObj).reduce(reducer(initialObj), []).join('&');
};
const Results = () => {
const router = useRouter()
useEffect(() => {
const query = router.query
router.replace(`https://www.somedomain.com/results?${objectToQueryString(query)}`)
}, [router])
return <></>
}
export default Results
I used the objectToQueryString based on Query-string encoding of a Javascript Object
To achieve this behavior, you need to capture the value of the query-parameter like this:
rewrites() {
return [
{
source: "/some-page",
has: [{ type: "query", key: "override_me", value: "(?<override>.*)" }],
destination: "/somewhere-else?override_me=:override",
},
];
}
By default, a rewrite will pass through any query params that the source URL may have.
In your case, you can simply have the following rewrite rule. All query params will be forwarded through the destination URL.
rewrites() {
return [
{
source: '/results',
destination: 'https://www.somedomain.com/results'
}
]
}

NextJS: New nonce on every application load while enabling CSP

I am trying to implement strict-dynamic CSP rules for my nextjs application. I want a new nonce value at every app load, but it is not happening. Currently a value is set for every build. Since I need the nonce value to set headers for the web pages, I need it in next.config.js.
Please let me know what I'm doing wrong.
next.config.js
const { v4 } = require('uuid');
const { createSecureHeaders } = require("next-secure-headers");
const crypto = require('crypto');
const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
const generatedNonce = function nonceGenerator() {
const hash = crypto.createHash('sha256');
hash.update(v4());
return hash.digest('base64');
}();
module.exports = function nextConfig(phase, { defaultConfig }) {
return {
publicRuntimeConfig: {
generatedNonce
},
headers: async () => {
return [
{
source: "/(.*)?",
headers: createSecureHeaders({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none"],
scriptSrc: [
...(phase === PHASE_DEVELOPMENT_SERVER ? ["'unsafe-eval'"] : []),
`'nonce-${generatedNonce}'`,
"'strict-dynamic'",
],
},
},
nosniff: "nosniff",
referrerPolicy: "no-referrer",
frameGuard: "sameorigin",
})
}
]
}
};
}

How to precache ALL pages with next-pwa

How would I go about precaching all the pages of my nextjs app using next-pwa?. Let's say I have the following pages:
/
/about
/posts
I want all of them to be precached so that they are all available offline once the app has been loaded the first time. At the moment I'm using a custom webpack config to copy the .next/build-manifest.json file over to public/build-manifest. Then once the app loads the first time, I register an activated handler that fetches the build-manifest.json file and then adds them to the cache. It works but it seems like a roundabout way of achieving it, and it depends somewhat on implementation details. How would I accomplish the same in a more canonical fashion?
At the moment, my next.config.js file looks like this
const pwa = require('next-pwa')
const withPlugins = require('next-compose-plugins')
const WebpackShellPlugin = require('webpack-shell-plugin-next')
module.exports = withPlugins([
[
{
webpack: (config, { isServer }) => {
if (isServer) {
config.plugins.push(
new WebpackShellPlugin({
onBuildExit: {
scripts: [
'echo "Transfering files ... "',
'cp -r .next/build-manifest.json public/build-manifest.json',
'echo "DONE ... "',
],
blocking: false,
parallel: true,
},
})
)
}
return config
},
},
],
[
pwa,
{
pwa: {
dest: 'public',
register: false,
skipWaiting: true,
},
},
],
])
And my service worker hook looks like this
import { useEffect } from 'react'
import { Workbox } from 'workbox-window'
export function useServiceWorker() {
useEffect(() => {
if (
typeof window !== 'undefined' &&
'serviceWorker' in navigator &&
(window as any).workbox !== undefined
) {
const wb: Workbox = (window as any).workbox
wb.addEventListener('activated', async (event) => {
console.log(`Event ${event.type} is triggered.`)
console.log(event)
const manifestResponse = await fetch('/build-manifest.json')
const manifest = await manifestResponse.json()
const urlsToCache = [
location.origin,
...manifest.pages['/[[...params]]'].map(
(path: string) => `${location.origin}/_next/${path}`
),
`${location.origin}/about`,
...manifest.pages['/about'].map((path: string) => `${location.origin}/_next/${path}`),
`${location.origin}/posts`,
...manifest.pages['/posts'].map((path: string) => `${location.origin}/_next/${path}`),
]
// Send that list of URLs to your router in the service worker.
wb.messageSW({
type: 'CACHE_URLS',
payload: { urlsToCache },
})
})
wb.register()
}
}, [])
}
Any help is greatly appreciated. Thanks.

Resources