Nextjs Nested Dynamic Routes (catch all?) - dynamic

I have searched many articles and posts regarding this topic, but they don't seem to make a lot of sense to me. I am new to Nextjs and dynamic routes in general, so I'm hoping someone can explain this clearly to me for my situation.
My page structure looks something like below. And what I want to do is combine the paths into one.
So I would have something like this:
-Page
--Category
---[slug].js
--Post
---[slug].js
I understand how this structure works. I would end up with two different routes, one that would be domain/category/[slug], and one that would be domain/post/[slug].
This is not what I want. I want it to be domain/category/[slug]/post/[slug].
I have also tried nesting the folder structure like below, though I'm not sure if that would work, or if I'm just not doing is correctly. Not to mention many variations and structures. I'm tired of beating my head against the wall.
-Page
--Category
---index.js
---[slug].js
---Post
----[slug].js
I'm fairly certain that the answer lies with using a catch all like [...params], I'm just not sure of the correct way to do, that makes sense to me.
An example of the code I'm using can be found here:https://github.com/adrianhajdin/project_graphql_blog/tree/main/pages
My structure is exactly the same, just folder and names may be different.
Edit: I should also note, I have no issue with changing the current site folder structure to accomplish this. I just don't want to change my CMS structure I currently have set up.
Edit 2: Based on feedback from #maxeth my page/[category]/index.js page looks like this minus the HTML:
export async function getStaticProps({ params }) {
const posts = await getCategoryPost(params.category);
return {
props: { posts },
revalidate: 60 * 20,
};
}
export async function getStaticPaths() {
const categories = await getCategories();
return {
paths: categories.map((el) => ({ params: { category: el } })),
fallback: false,
};
}
Although I'm not sure if getStaticProps/const posts = await getCategoryPost(params.category) is correct. Should it be (params.category)?
It should also be noted that getCategoryPost and getCategories queries are being imported if that wasn't obvious already. I'm sure it is though. They can be found here:
https://github.com/adrianhajdin/project_graphql_blog/blob/main/services/index.js
I'm not sure if the slug in the CMS also needs to be renamed to reflect the [category] directory. Currently, it's just named Slug.
Edit 3: The path up through pages/category/[category] works fine. It's when I get to pages/category/[category]/post/[slug] that I get a 404.
Post/[slug] getstaticprops and getstaticpaths looks like the following:
export async function getStaticProps({ params }) {
const data = await getPostDetails(`${params.category}/${params.slug}`);
return {
props: {
post: data,
},
};
}
export async function getStaticPaths() {
const posts = await getPosts();
return {
paths: posts.map(({ node: { slug } }) => ({
params: { category: slug.category, slug: slug.slug },
})),
fallback: true,
};
}
The Post queries look like this:
export const getPosts = async () => {
const query = gql`
query MyQuery {
postsConnection(orderBy: createdAt_DESC) {
edges {
cursor
node {
content {
html
raw
text
}
author {
bio
name
id
photo {
url
}
}
createdAt
slug
title
excerpt
featuredImage {
url
}
categories {
name
slug
}
}
}
}
}
`;
const result = await request(graphqlAPI, query);
return result.postsConnection.edges;
};
export const getPostDetails = async (slug) => {
const query = gql`
query GetPostDetails($slug: String!) {
post(where: { slug: $slug }) {
content {
html
text
raw
}
title
excerpt
featuredImage {
url
}
author {
name
bio
photo {
url
}
}
createdAt
slug
content {
html
raw
text
}
categories {
name
slug
}
}
}
`;
const result = await request(graphqlAPI, query, { slug });
return result.post;
};
What the heck am I missing? It has to be something really simple. I'm just not smart enough to see it.
Edit 4: The 404 was caused by a casing mistake. Where I was typing /post/[slug].js instead of /Post/, since my directory was named Post.

You can achieve this with the following structure:
└── pages
└── category
├── index.js // this is the "overview page" of all categories
└── [category]
└── index.js // this is the "overview page" of a specific category
└── post
└── [slug].js // this is an actual article page for that specific category
Note that you cannot use the dynamic-route name [slug] twice inside the folder category, because we will have to return objects with the path names as keys, e.g. [category] -> {category: ...}, [slug] -> {slug: ...}, so they have to be unique for next to tell for which folder you are returning the slugs inside getStaticPaths.
Now if you use getStaticPaths, you need to return the specific slug for the page, as well as the "slugs" of the potential parent page inside params: {...} in order for the pages to be pre-generated.
So inside category/[category]/index.js, you would need to do something like this:
export async function getStaticPaths(context){
// [...] fetch all categories from CMS
const allCategories = await getAllCategoriesFromCMS();
// allCategories includes all of the categories from your CMS:
// e.g. ["category-A", "category-B", "category-C", ...]
return {
paths: allCategories.map(el => ({ params: { category: el } }))
};
}
Which is then accessible within getStaticProps like this:
export async function getStaticProps({params}){
console.log(params.category) // can be any of the dynamic categories you fetched and returned inside getStaticPaths
}
...
And the same concept applies to category/[category]/post/[slug].js, just that you now have to return/handle 2 "slugs" [category] and [slug] inside getStaticPaths and getStaticProps:
export async function getStaticPaths(context){
// [...] fetch all posts and the specific categories they belong to from CMS
const allPosts = await getAllPostsFromCMS();
// allPosts includes all of the posts/articles from your CMS, assuming the posts have a relationship with their respective category:
/* e.g. [
{category: "category-A", postSlug:"some-article-for-category-A", author: "some author", articleContent: "..."},
{category: "category-B", postSlug:"some-article-for-category-B", author: "some author", articleContent: "..."},
]
*/
return {
paths: [ // once again, we need to call the keys 'category' and 'slug' because we called the dynamic routes '[category]' and '[slug].js'
allPosts.map(el => ({ params: { category: el.category, slug: el.postSlug } }))
],
};
}
and in getStaticProps:
export async function getStaticProps({ params }) {
console.log(params);
// You are guaranteed to have matching categories and slugs in here, for example
// {category: category-A, slug:some-article-for-category-A}
// or {category: category-B, slug:some-article-for-category-B}
// You can use these params to fetch the article entry from the CMS
const article = await fetchFromCMS(`${params.category}/${params.slug}`);
// [...]
}
Now, for example, one of the articles will be accessible under localhost:3000/category/category-A/post/some-post-for-category-A

Related

What's the best way to transform vue-query results to deal with belongsTo relationships?

So I'm using vue-query to get data from my API. The current way I'm doing that looks a little like this. I have a folder in src called hooks, and it may contain a file such as usePosts.ts. That file contains code like this:
import { useQuery, useMutation, useQueryClient } from "vue-query"
import axios, { AxiosError } from "axios"
import {
performOptimisticAdd,
handleMutateSuccess,
handleMutateError,
} from "./handlers"
export interface Post {
id: number
title: string
body: string
user: number // user_id
}
export function usePostsListQuery() {
return useQuery<Post[], AxiosError>(
"posts",
() => axios.get("/v1/posts").then(resp => resp.data),
{ placeholderData: [] }
)
}
export function useAddPostMutation() {
const client = useQueryClient()
return useMutation<Post, AxiosError>(
post => axios.post("/v1/posts", post).then(resp => resp.data),
{
onMutate: performOptimisticAdd(client, "posts"),
onSuccess: handleMutateSuccess(),
onError: handleMutateError()
}
)
}
Of course I'm not showing all the code, for brevity.
Now in my Vue components, I'm often doing something like this:
<script setup>
import { usePostsListQuery, useAddPostMutation } from "#/hooks/usePosts";
import { useUsersListQuery } from "#/hooks/useUsers";
const { data: posts } = $(usePostsListQuery())
const { data: users } = $(useUsersListQuery())
const { mutate: addPost } = $(useAddPostMutation())
const postsWithUsers = $computed(() => posts.map(
post => ({ ...post, user: users.find(user => user.id === post.user) })
))
const addPostWithUserId = (newPost: Post) => addPost({ ...newPost, user: newPost.user.id })
</script>
Because I want to be able to directly access the user associated with a post. And of course, the way I'm doing it works. But it doesn't seem right to do that transformation inside a Vue-component. Because that means I need to repeat that same code inside every new Vue-component.
So I'm wondering what would be the best place to do this transformation. One obstacle is that useQuery() may only be called during / inside the setup() function. So I'm a bit limited in terms of where I'm allowed to call these queries.
Maybe I could just put it inside usePosts.ts? But is that really the best place? I can imagine that it might make all my hooks very messy, because then every hook suddenly has TWO responsibilities (talking to my API, and transforming the output and input). I feel like that breaks the single responsibility principle?
Anyhow, this is why I'd love to hear some of your opinions.

Hide route params from the url with react-navigation

I am adapting a ReactNative mobile app to ReactNative Web. The app is done with react-navigation.
Currently, every time I set the params of a route (either through navigate or setParams), those params show in the URL. I end up with bad looking URLs like so:
http://localhost:19006/me/undefined?user=%5Bobject%20Object%5D
Either that or the URL contains irrelevant data for the user and generally looks messy.
Is there a way to not show the route params inside the URL?
You should re-consider if params is an appropriate place to have this data if you don't want in the URL. That you think that the URL contains irrelevant data is a sign that the data doesn't belong in the navigation state.
If you visit a screen directly from a URL, it should render the same as when you navigated to it porgrammatically. If you're passing something in params, it means that that information is needed for the screen to render correctly, and if you remove it from the URL, then that information is lost. Consider that it's possible to open the page directly from a URL, either on Web or with a deep link. If the required data isn't available, then it's not going to work.
In your case, seems like you're passing a full user object (maybe not, hard to say without code). Ideally, this object should be in your global store instead of params, and you should only pass the user id in params. Then you should gran the full object from your global store using that id, or trigger a data fetch if the objet isn't fetched yet (if needed).
You didn't specify the version of React Navigation in your question. In v5, you can customize how your params are stringified to URLs, and how they are parsed back using the stringify and parse options:
const linking = {
screens: {
Profile: {
path: 'user/:id',
parse: {
id: (id) => id.replace(/^#/, '')
},
stringify: {
id: (id) => `#{id}`,
},
},
},
};
This should help you with making URLs look prettier, and handle cases where params are not simple strings. However you can't omit params from the URL altogether since they are necessary data for the screen to render.
More details: https://reactnavigation.org/docs/configuring-links#passing-params
satya164's answer is definitely the proper path to follow. Just wanted to also post this solution as it is a direct answer to the question, even if not the most advisable.
import { getPathFromState} from "#react-navigation/native";
const linking = {
screens: {
...
},
getPathFromState(state, config){
let path = getPathFromState(state, config);
const index = path.indexOf("?")
if(index>=0){
path = path.substr(0, index);
}
return path;
}
};
NOTE: To use this solution you need to make sure that every object and function parameters are optional, otherwise if you reload the page you will get an error.
I removed every object and function from the url adding this custom getPathFromState to linkingOptions:
const linking = {
config: {
screens: {
...
}
},
getPathFromState: (state, options) => {
const cleanState = {
...state,
routes: state.routes.map(route => {
if(!route.params) {
return route
}
const cleanParams = {}
for(const param in route.params) {
const value = route.params[param]
if(typeof value !== "object" && typeof value !== "function") {
cleanParams[param] = value
}
}
return {
...route,
params: cleanParams,
}
}),
}
return getPathFromState(cleanState, options) //imported from #react-navigation/native
},
}
You can pass the parameters the component needs as a prop
like this
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
then in Details Component
const { itemId , otherParam} = route.params;
Alternatively, if you use Redux/Mobx or any other global state management
You can pull the data from there and not pass it through the URL
then get data with the help connect or with useSelector hooks

How to dynamically set query parameters with AWS AppSync SDK for React-Native

Background: I'm working on building a mobile app with react-native, and am setting up AWS's AppSync for synchronizing the app with cloud data sources.
The challenge: I have a view which shows all items in a list. The list's ID is passed in as a prop to the component. I need to use that list ID to query for the items of that list. I have the query working fine if I hard-code the list ID, but I'm having a hard time figuring out how to dynamically set the list ID for the query when props update.
Here's what I have working (with a hard-coded ID of testList01) in my ListPage component:
const getListItems = id => gql`
query getListItems {
getListItems(listID: ${id}) {
reference_id,
quantity,
}
}
`;
export default graphql(getListItems('testList01'), {
options: {
fetchPolicy: 'cache-and-network',
},
props: props => ({
listItems: props.data ? props.data.getListItems : [],
data: props.data,
}),
})(withNavigationFocus(ListPage));
I would like to be able to dynamically set which list to look up the items for based on a list ID, which is being passed in from props. Specifically, I'm using react-navigation to enter the ListPage, a view where a user can see the items on a List. So here's the code that gets executed when a user clicks on a list name and gets routed to the ListPage component:
handleListSelection(list: Object) {
const { navigation, userLists } = this.props;
navigation.navigate('ListPage', {
listID: list.record_id,
listName: list.list_name,
userLists,
});
}
From my previous (pre-AppSync/GraphQL) implementation, I know that I can access the list ID in ListPage via this.props.navigation.state.params.listID. I would like to be able to use that in my AppSync query, but because the query is created outside the component, I'm unable to access the props, and so am struggling to get the ID.
Got this working using a package called react-apollo-dynamic-query which I found here. The author of that package also links directly to a simple function for doing what I'm trying to do here.
Essentially it just wraps the regular graphql call in a simple way that exposes the props so they can be passed down to the query.
My code now looks likes this (which I have below my definition of the ListPage component, in the same file):
const getListItems = props => {
const listID = props.navigation.state.params.listID;
return gql`
query getListItems {
getListItems(listID: "${listID}") { // Note the variable being wrapped in double quotes
reference_id,
quantity,
}
}
`;
};
const config = {
options: {
fetchPolicy: 'cache-and-network',
},
props: props => ({
listItems: props.data ? props.data.getListItems : [],
}),
};
const MyApolloComponent = graphqlDynamic(getListItems, config)(ListPage);
export default MyApolloComponent;
It should work like this:
const getListItems = (id) => {
return gql`
query getListItems {
getListItems(listID: ${id}) {
reference_id,
quantity,
}
}
`;
}
Call this getListItems like the below
export default graphql(getListItems(id), { //from where ever you want to send the id
options: {
fetchPolicy: '
......
I have not tested this code. Please update if this works. Although I am quite sure that it works.

How can I access data in asyncData with Nuxt

I'm attempting to build a server-side sortable table with Nuxt, and I'd like to be able to specify the default sort column and direction in my Vue data, and access that in my asyncData function. Something like this:
<script>
export default {
async asyncData ({ $axios, params }) {
const things = await $axios.$get(`/api/things`, {
params: {
sort_column: this.sortColumn,
sort_ascending: this.sortAscending,
}
});
return { things };
},
data () {
return {
sortColumn: 'created_at',
sortAscending: true
}
},
// ...
}
</script>
But it appears that data is not yet available, as this.sortColumn and this.sortAscending are not defined. How can I access these defaults when asyncData runs while also allowing them to be changed when the user interacts with the page. (Alternatively, what's a better way to structure this?)
Note: This question was asked here, but the accepted answer is not relevant to this situation.
You can just return it all into asyncData. E.g. something like this:
async asyncData ({ $axios, params }) {
const sortColumn = 'created_at'
const sortAscending = true
const things = await $axios.$get(`/api/things`, {
params: {
sort_column: sortColumn,
sort_ascending: this.sortAscending,
}
});
return { things, sortColumn, sortAscending };
},
And it will behave like you want.

Select a layout for dynamically generated nuxt page

I'm building a project where all my data - including page routes - comes from a GraphQL endpoint but needs to be hosted via a static site (I know, I know. Don't get me started).
I've managed to generate routes statically from the data using the following code in nuxt.config.js:
generate: {
routes: () => {
const uri = 'http://localhost:4000/graphql'
const apolloFetch = createApolloFetch({ uri })
const query = `
query Pages {
pages {
slug
template
pageContent {
replaced
components {
componentName
classes
body
}
}
}
}
`
return apolloFetch({ query }) // all apolloFetch arguments are optional
.then(result => {
const { data } = result
return data.pages.map(page => page.slug)
})
.catch(error => {
console.log('got error')
console.log(error)
})
}
}
The problem I am trying to solve is that some pages need to use a different layout from the default, the correct layout to use is specified in the GraphQL data as page.template but I don't see any way to pass that information to the router.
I've tried changing return data.pages.map(page => page.slug) to:
return data.pages.map(page => {
route: page.slug,
layout: page.template
})
but that seems to be a non-starter. Does anyone know how to pass a layout preference to the vue router?
One way would be to inject the data into a payload
https://nuxtjs.org/api/configuration-generate#speeding-up-dynamic-route-generation-with-code-payload-code-
This will allow you to pass generate information into the route itself.