NextJS case insensitive route for SSG pages - next.js

I am using NextJS to translate a CSV of data into static pages. Each page is pages/[slug].jsx. I'm calling toLowerCase() on the slug value inside [slug].jsx getStaticPaths() and getStaticProps() functions. The generated pages are lowercase. e.g. /ab101, /ab102, /cc500 all resolve to the pages/[slug].jsx page.
Unfortunately, people might hand type the url and may use caps or mixed case for the slug value, currently resulting in a 404.
QUESTION: How can I make routing case insensitive with respect to the slug value?
UPDATE
When I return fallback: true from getStaticPaths(), my [slug].jsx file is hit even when there is not an exact path match. I can then check isFallback as illustrated by Anish Antony below.
Additionally, the items param that is passed to my page will be undefined when the page wasn't found. The router's pathname value is "/[slug]" and not the value of "slug". However, there is an asPath value which contains useful data, e.g. /mixedCaseSlug?param=value&foo=bar.
When the page renders, I check if it's a fallback. If it is, show a LOADING... message. Next will display that and call getStaticProps() to generate the "missing" page. You'll then re-render with the page data. In the event that getStaticProps couldn't get page data, I push a path that will lead to the built-in 404 page.
export default function Page({ item }) {
const { isFallback, push } = useRouter()
const hasPageData = item && Object.keys(item).length > 0
useEffect(() => {
if (!isFallback && !hasPageData) {
push('/page-not-found/error')
}
}, [isFallback, hasPageData])
const loadingMsg = <div>Loading...</div>
const notFoundMsg = <div>Page not found</div>
return isFallback ? loadingMsg : hasPageData ? <Item item={item} /> : notFoundMsg
}
I needed to update getStaticProps() to lowercase the slug param, as it may now be mixed case, but we want to find our page data. And I needed to allow for the case when there really is no data for the slug.
export async function getStaticProps({ params }) {
const { slug } = params
const item = data.find(o => o.Practice_Code.trim().toLowerCase() === slug.toLowerCase())
return {
props: {
item: item ? item : {}
}
}
}
This all seems very kludgy, so I'm still wondering if there is a better way.

NextJS routes are case sensitive.You can use fallback property in getStaticPaths to catch the routes which aren't in the same case as the one provided by default in getStaticPaths.
Edit: I have updated the answer based on the discussion with Dave.
We can give fallback:true or fallback:"blocking" , if we give fallback:true we we can show a custom component which will be displayed till the time page is loaded.For fallback:"blocking" new paths not returned by getStaticPaths will wait for the HTML to be generated,
When we give fallback:true or "blocking" static page will be generated when the user first access the site and the generated page will be served for further visits.
Sample code
export async function getStaticPaths() {
const idList = await fetchAllIds();
const paths = [
];
idList.forEach((id) => {paths.push(`/posts/${id}`)})
return { paths, fallback: true };
}
What we have note is our code in getStaticProps should be case insensitive to get the data irrespective of the one provided in url.
export async function getStaticProps({ params }) {
const { slug } = params;
try {
/// This fetch api should be able to fetch the data irrespective of the case of the slug
or we should convert it to the required case before passing it as a parameter to API
const data= await fetchSlugDetails(slug);
//Same logic should be performed if you are getting data filtered based on slug from an existing array.
return data? { props: { data} } : { notFound: true };
} catch (error) {
console.error(error);
return { notFound: true };
}
}
Note: You have to handle the notfound case and fallback case if you are using fallback:true. For fallback you can get the value isFallback from next/router while the page is being static generated

Related

useFetch of Nuxt 3 sends same request during refresh instead of updating URL

I try to do a simple thing: At route change, refetch data.
Here is my link:
<li v-for="category in categories" :key="category.id" class="mb-1">
<NuxtLink :to="{ query: { cat: category.slug } }">
{{category.title}}
</NuxtLink>
</li>
And my request:
<script setup>
const route = useRoute()
const { data:categories } = await useFetch('http://127.0.0.1:8000/api/tasks/category/', {
key: route.fullPath,
initialCache: false
})
const { data:tasks, refresh } = await useFetch(`http://127.0.0.1:8000/api/tasks/?cat=${route.query.cat}`, {
key: `tasks:${route.query.cat}`,
initialCache: false
})
watch(() => route.query.cat, () => refresh())
</script>
So, at click, url is well changed:
http://localhost:3000/tasks?cat=category1
http://localhost:3000/tasks?cat=category3
http://localhost:3000/tasks?cat=category2
But requests are the same (from Django DRF backend):
GET http://127.0.0.1:8000/api/tasks/?cat=category1
GET http://127.0.0.1:8000/api/tasks/?cat=category1
GET http://127.0.0.1:8000/api/tasks/?cat=category1
It seems that it keeps the first category, even with initialCache: false
As mentioned in the comments, the URL should not be provided as string
const { data:tasks, refresh } = await useFetch(`http://127.0.0.1:8000/api/tasks/?cat=${route.query.cat}`, {
key: `tasks:${route.query.cat}`,
initialCache: false
})
but as a function returning a string:
const { data:tasks, refresh } = await useFetch(() => `http://127.0.0.1:8000/api/tasks/?cat=${route.query.cat}`, {
key: `tasks:${route.query.cat}`,
initialCache: false
})
useFetch is "freezing" the API URL, the changes you make to the string directly will not be reflected. If you want to add parameters to your API URL, as an alternative to using a function, you can use the query option of useFetch. This option is reactive, that is when the ref with your route is changing, the query will reflect this update.
For your case, this will work:
const cat = ref('your_value')
const { data:tasks, refresh } = await useFetch('http://127.0.0.1:8000/api/tasks', {
query: { cat }
})
This results in http://127.0.0.1:8000/api/tasks?cat=your_value
You don't need to mess with the key option, as the key will be auto generated, and you can also leave the cache alone.
The query option is not well documented yet, as discussed in this nuxt issue. I've created a pull request on nuxt/framework to have it reflected in the documentation. Please see a full explanation below:
Using the query option, you can add search parameters to your query. This option is extended from unjs/ohmyfetch and is using ufo to create the URL. Objects are automatically stringified.
const param1 = ref('value1')
const { data, pending, error, refresh } = await useFetch('https://api.nuxtjs.dev/mountains',{
query: { param1, param2: 'value2' }
})
This results in https://api.nuxtjs.dev/mountains?param1=value1&param2=value2

Access data from getServerSideProps in custom document Next.js

I'm using getServerSideProps to fetch details of an event. My understanding is that this data will get fetched on the server, then some sort of prerendering will happen, and then a context object is passed into _document.js. I know _document.js will get rendered on every request, but I would like to add a class name to my html if that server data is present (to prevent flashing when doing this client side).
Here is my getServerSideProps function:
export async function getServerSideProps(context) {
const shortId = context.params.short_id
const event = await GETrequest({ endpoint: API_PUBLIC_EVENT({ shortId }) })
return {
props: {
event,
shortId
} // will be passed to the page component as props
}
}
When I'm within _document.js, I'm able to see that the results of my getServerSideProps are available, as seen when I log the context in getInitialProps like this:
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
console.log(initialProps)
// can see html that has been processed using my data
}
...
I can think of some hacky solutions of getting my className in that initialProps.html, but I'm wondering if there is an easier way.
Is there a way I can structure this so that my data will be more easily available in _document.js, passed from getServerSideProps()?
Here is a hacky way to get your page's getServerSideProps() data into your _document.js, before the app is loaded.
So, as I mentioned you can do some funky stuff with the prerendered html that comes in to use the getServerSideProps() data. This is a hacky solution and very unreliable. In my case however, I'm simply adding a non-consequential className to my document (with a fail-safe on the front-end), so even if this fails everything will be okay.
Use an abundance of caution if you decide to use this.
This is how it works:
I have a getServerSideProps() on a page. In this function, I fetch an event and pass it into my page component. In this component, I add an empty div that I know will be rendered, appending the information I need. This information came from my getServerSideProps().
<div data-meta-event-theme={event?.theme?.preset}></div>
// let's say theme preset = light
Then in my _document.js, within a getInitialProps(ctx) function, I first check to see if this is a server request. If it's not, skip (build will fail otherwise). Then I check to see if the request is coming from the appropriate route. My route is structured like /j/[shortId], and it's the only page using /j/. So before any logic gets triggered, I check to make sure we're on the right page.
const isInviteScreen = ctx.req.url.indexOf('/j/') > -1
If we're on the right page, then we can parse the value from our html, and do what we want with it.
if (isInviteScreen) {
const html = initialProps.html
// whatever you set your data to in the html
// make sure to keep the opening quote "
const key = 'data-meta-event-theme="'
// you don't have to touch this
const begin = html.indexOf(key)
const sub = html.substring(begin + key.length)
const closingQuote = sub.indexOf('"')
const value = sub.substring(0, closingQuote)
console.log(value)
// logs 'light'
}
At this point, I'm able to append that value as a className onto my html, and voila!, no more flashing.
Here is a more full _document.js:
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
// Make sure this operation is on the server and has a request
if (ctx.req) {
// Make sure we're only doing this for the correct route
const isInviteScreen = ctx.req.url.indexOf('/j/') > -1
if (isInviteScreen) {
const html = initialProps.html
// Your HTML key here, make sure to keep quote
const key = 'data-meta-event-theme="'
// Don't have to touch this
const begin = html.indexOf(key)
const sub = html.substring(begin + key.length)
const closingQuote = sub.indexOf('"')
// Value that was sent from the page with getServerSideProps()
const value = sub.substring(0, closingQuote)
return {
...initialProps,
preference: { theme: value }
}
}
}
// Default return just in case
return {
...initialProps,
preference: { theme: 'light }
}
}
render() {
const theme = this.props.preference.theme
return (
<Html data-theme={theme} className={theme} lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
Since this data seems somewhat accessible between the two, I'm hoping there is a more official way to do this?

Get values from SvelteKit's $app/stores outside of the lifecycle of a component

My Svelte components import readable stores like this:
import { classes, locations, schedule } from 'stores.ts'
In stores.ts, I want to build the URL for fetch dynamically using page.host from $app/stores.
// Note: this is not a Svelte component; it's stores.ts
import { readable } from 'svelte/store'
import { getStores } from '$app/stores'
const { page } = getStores()
let FQDN
page.subscribe(({ host }) => {
FQDN = host
})
const getArray = async (url) => {
const response: Response = await fetch(url)
if (!response.ok) throw new Error(`Bad response trying to retrieve from ${url}.`)
return await response.json()
}
const getReadableStore = (url: string) => readable([], set => {
getArray(`http://${FQDN}${url}`)
.then(set)
.catch(err => console.error('Failed API call:', err))
return () => {}
})
export const classes = getReadableStore('/api/class/public.json')
export const locations = getReadableStore('/api/location/public.json')
export const schedule = getReadableStore('/api/schedule/public.json')
The sixth line throws this error...
Error: Function called outside component initialization
at get_current_component (/Users/nates/dev/shy-svelte/node_modules/svelte/internal/index.js:652:15)
at Proxy.getContext (/Users/nates/dev/shy-svelte/node_modules/svelte/internal/index.js:685:12)
at Module.getStores (/.svelte-kit/dev/runtime/app/stores.js:17:26)
at eval (/src/stores.ts:6:38)
at instantiateModule (/Users/nates/dev/shy-svelte/node_modules/#sveltejs/kit/node_modules/vite/dist/node/chunks/dep-e9a16784.js:68197:166)
Two questions...
What is the correct way to get page values from $app/stores outside of the context of a component? Is this possible? Answer from below: No, this is not possible outside the context of a component.
If I'm accessing a SvelteKit site, let's say http://localhost:3000/something or https://example.com and a Svelte component loads a readable store from stores.ts, is there a way in stores.ts to determine whether the original page request that loaded the component (which loaded from stores.ts) was http or https? Answer from below: No, this is not possible in stores.ts - only from a component.
UPDATE: Based on the feedback, I'm going to set a value in my .env called VITE_WEB_URL=http://localhost:3000 and change it for the production system. This cuts down on the number of lines of code and may be a better practice (comments welcome)...
// revised stores.ts
import { readable } from 'svelte/store'
const { VITE_WEB_URL } = import.meta.env
const getArray = async (url) => {
const response: Response = await fetch(url)
if (!response.ok) throw new Error(`Bad response trying to retrieve from ${url}.`)
return await response.json()
}
const getReadableStore = (url: string) => readable([], set => {
getArray(`${VITE_WEB_URL}${url}`)
.then(set)
.catch(err => console.error('Failed API call:', err))
return () => {}
})
export const classes = getReadableStore('/api/class/public.json')
export const locations = getReadableStore('/api/location/public.json')
export const schedule = getReadableStore('/api/schedule/public.json')
Extract from https://kit.svelte.dev/docs#modules-$app-stores
Because of that, the stores are not free-floating objects: they must be accessed during component initialisation, like anything else that would be accessed with getContext.
Therefore, since the readable store is bound to the context of a svelte component, I suggest you subscribe either way ($ or .subscribe) inside the component of the SvelteKit website and then send the protocol value (http or https) as parameter when it updates so that stores.ts stores it in a variable.
However, it looks like SvelteKit does not provide the protocol value, so parse the client side window.location.href in the page subscription and then send it.
Referencing a svelte store can be done everywhere.
Using the $: shorthand syntax, however, only works within a component.
$: BASE = `http://${$page.host}`
SvelteKit appears to delegate this to fetch indeed

NextJS special characters routes do not work from browser

Using NextJS, I am defining some routes in getStaticPaths by making an API call:
/**
* #dev Fetches the article route and exports the title and id to define the available routes
*/
const getAllArticles = async () => {
const result = await fetch("https://some_api_url");
const articles = await result.json();
return articles.results.map((article) => {
const articleTitle = `${article.title}`;
return {
params: {
title: articleName,
id: `${article.id}`,
},
};
});
};
/**
* #dev Defines the paths available to reach directly
*/
export async function getStaticPaths() {
const paths = await getAllArticles();
return {
paths,
fallback: false,
};
}
Everything works most of the time: I can access most of the articles, Router.push works with all URLs defined.
However, when the article name includes a special character such as &, Router.push keeps working, but copy/pasting the URL that worked from inside the app to another tab returns a page:
An unexpected error has occurred.
In the Network tab of the inspector, a 404 get request error (in Network) appears.
The component code is mostly made of API calls such as:
await API.put(`/set_article/${article.id}`, { object });
With API being defined by axios.
Any idea why it happens and how to make the getStaticPaths work with special characters?
When you transport values in URLs, they need to be URL-encoded. (When you transport values in HTML, they need to be HTML encoded. In JSON, they need to be JSON-encoded. And so on. Any text-based system that can transport structured data has an encoding scheme that you need to apply to data. URLs are not an exception.)
Turn your raw values in your client code
await API.put(`/set_article/${article.id}`)
into encoded ones
await API.put(`/set_article/${encodeURIComponent(article.id)}`)
It might be tempting, but don't pre-encode the values on the server-side. Do this on the client end, at the time you actually use them in a URL.

Nextjs page goes to 404 on refresh

I'm using nextjs and graphql for a shopify POC.
I have a component that shows a list of products with links on them that point to the product page
<Link
as={`${handle}/product/${url}`}
href={`/product?id=${item.id};`}>
<a>{item.title}</a>
</Link>
handle is the collection name so the url in the browser will look like
http://localhost:3000/new-releases/product/Plattan-2-Bluetooth but behind the scenes its really just using a page called products and i'm passing the product id.
Now in product.js (pasted below) i'm getting the query string value of the id and doing another query to get the product. All works fine but then if i hit refresh or copy and paste the url into a new window i get 404.
I know this is something to do with routing but i'm not sure what i need to do to fix this. Thanks
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
class product extends Component {
static async getInitialProps({query}) {
console.log("query", query)
return query ? { id: query.id.replace(';', '') } : {}
}
render() {
const PRODUCT_FRAGMENT = gql`
fragment ProductPage on Product {
title
description
descriptionHtml
id
images(first: 10, maxWidth: 600) {
edges {
node {
id
altText
originalSrc
}
}
}
}
`;
const PRODUCT_FETCH_QUERY = gql`
query PRODUCT_FETCH_QUERY {
node(id: "${this.props.id}") {
__typename
...ProductPage
}
}
${PRODUCT_FRAGMENT}
`;
return (
<div>
<Query query={PRODUCT_FETCH_QUERY}>
{({ data, error, loading }) => {
console.log("data", data)
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return null}
}
</Query>
</div>
);
}
}
product.propTypes = {
};
export default product;
You can try these in a file called next.config.js in the root of your project
module.exports = {
trailingSlash: true
}
Check this link
This is because when you use the next/link component the href prop has the "real" URL to the page with the query parameter for the item ID set. This means that on the client (the browser), Next.js can load the right page (your product.js page) with the parameter for your data query.
But when you load from the server, either by reloading the page or opening it in a new window, Next.js doesn't know what page to load and in this case I think it will try to find the file ./pages/new-releases/product/Plattan-2-Bluetooth.js, which of course doesn't exist.
If you want to have these kinds of URLs you have to make sure the request gets routed to the right page file (./pages/product.js) on the server as well. You can do this by by creating a custom server. There are a bunch of examples in the Next.js repo including one using Express. This is also covered in the "Learn" section of there website in a tutorial called Server Side Support for Clean URLs
If you decide to use Express, you will end up with something like:
server.get('/:collection/product/:productUrl', (req, res) => {
return app.render(req, res, '/product', { productUrl: req.params.productUrl})
})
This will render the product page, and you'll have productUrl available on the query object in getInitialProps(). Of course now you will need to fetch your data with that instead of the product id.

Resources