How to get post by slug with Sanity GraphQL and NextJS - next.js

I am using Next JS 13 and Sanity 3 GraphQL API to build a simple portfolio site. My problem is to get a single post/project by slug instead of id.
The id url ends up looking like:
http://127.0.0.1:3000/projects/093e9421-3dec-4b22-a106-63dd31b0e685
but I want it to use the slug instead.
This is the getStaticFunction function
export async function getStaticProps({ params }: any) {
const GET_PROJECTS = gql`
query SingleProject($slug: String) {
Project(slug: $slug) {
title
_id
slug {
current
}
bodyRaw
summary
category {
title
}
projectImage {
asset {
url
}
}
}
}
`;
const response = await client.query({
query: GET_PROJECTS,
variables: {
slug: params.slug.current,
},
});
const project = response.data?.Project;
return {
props: {
project,
},
};
}
but this gives me:
ApolloError: Unknown argument "where" on field "Project" of type "RootQuery".
Field "Project" argument "id" of type "ID!" is required, but it was not provided.
I also tried to query using the where like so:
query SingleProject($slug: String!) {
Project(where: { slug: { current: { eq: $slug } } }) {
title
}
}
but I get a Unkown arugment "where" on field "Project" of type "RootQuery"

Related

Generate paginated URLs like ?page=2, ?page=3 etc. in NextJS getStaticPaths and access the query params in getStaticProps

I am building a NextJS based site with multiple locales (different domains) where the data comes from storyblok CMS (folder level translation).
I am trying to figure out the best approach to statically generate the paginated URLs for the blog and since the data is known at build time, I figured the best approach would be to generate all URLs in getStaticPaths and then fetch the data for each Page in getStaticProps. This works fine for routes without parameters but when returning a page parameter along with the slug parameter in getStaticPaths, I cannot access it in getStaticProps.
I know that query params cannot be accessed in getStaticPaths because we cannot know the custom querys at buildtime, but in this specific case, we actually can since these paths are generated in getStaticProps.
pages/[[...slug]].jsx
import {
useStoryblokState,
getStoryblokApi,
StoryblokComponent,
} from "#storyblok/react";
export default function Page({
story,
locale,
locales,
defaultLocale,
stories,
}) {
story = useStoryblokState(story, {
// language: locale,
});
return (
<div>
<StoryblokComponent
blok={story.content}
storyData={story}
stories={stories}
/>
</div>
);
}
export async function getStaticProps({
locale,
locales,
defaultLocale,
params,
}) {
console.log(params.slug); // This logs the slug
console.log(params.page); // This logs undefined
console.log(params.query.page); // This logs undefined
// Empty slug on front page
// Make sure root element page pr folder are selected in storyblok
let slug = params.slug ? params.slug.join("/") : "";
let sbParams = {
version: "draft",
resolve_relations: relationsResolvers,
language: locale,
};
let { data } = await getStoryblokApi().get(
`cdn/stories/${locale}/${slug}`,
sbParams
);
let sbIndexParams = {
version: "draft",
resolve_relations: relationsResolvers,
per_page: 10,
page: params.page || 1,
starts_with: `${locale}/${slug}`,
sort_by: "first_published_at:desc",
language: locale,
filter_query: {
component: {
in: "page,post,case,template",
},
},
};
/* fetch an array of stories if page is startpage */
let storiesData = null;
if (data.story.is_startpage) {
storiesData = await getStoryblokApi().get(`cdn/stories`, sbIndexParams);
}
return {
props: {
story: data ? data.story : false,
key: data ? data.story.id : false,
stories:
data.story.is_startpage && storiesData
? storiesData.data.stories
.filter((story) => story.is_startpage == false)
.map((story) => {
return {
name: story.name,
created_at: story.created_at,
published_at: story.published_at,
id: story.id,
uuid: story.uuid,
slug: story.slug,
full_slug: story.full_slug,
is_startpage: story.is_startpage,
content: {
cover: story.content.cover ?? null,
cover_image: story.content.cover_image ?? null,
author: story.content.author ?? null,
category: story.content.category ?? null,
},
};
})
: false,
locale,
locales,
defaultLocale,
},
revalidate: 3600,
};
}
export async function getStaticPaths({ locales }) {
let { data } = await getStoryblokApi().get("cdn/links/", {
is_folder: false,
filter_query: {
component: {
in: "page,post,case,template",
},
},
});
let paths = [];
Object.keys(data.links).forEach((linkKey) => {
if (data.links[linkKey].is_folder) {
return;
}
// get array for slug because of catch all
const slug = data.links[linkKey].slug;
let splittedSlug = slug.split("/");
const linkLocale = splittedSlug[0];
splittedSlug.shift();
if (splittedSlug == "") splittedSlug = false;
// create additional languages
for (const locale of locales) {
if (linkLocale === locale) {
paths.push({ params: { slug: splittedSlug }, locale });
}
}
});
// pagination route generation on custom post types like posts and cases
const per_page = 10;
const startPagesArr = Object.values(data.links)
.map((obj) => obj)
.filter((obj) => obj.is_startpage == true)
.filter((obj) => obj.slug.split("/").length > 2);
// make a loop that loops through all startpages and fetches all stories that are children of that startpage
for (const startPage of startPagesArr) {
let res = await getStoryblokApi().get("cdn/links/", {
is_folder: false,
starts_with: startPage.slug,
paginated: 1,
page: 1,
per_page: per_page,
sort_by: "first_published_at:desc",
filter_query: {
component: {
in: "post,case,template",
},
},
});
let totalPages = Math.ceil(res.total / per_page);
let splittedSlug = startPage.slug.split("/");
const linkLocale = splittedSlug[0];
splittedSlug.shift();
if (splittedSlug == "") splittedSlug = false;
// ... Loop through locales and push the paginated pages to the paths Array
for (const locale of locales) {
if (linkLocale === locale) {
for (let i = 2; i <= totalPages; i++) {
paths.push({
params: {
slug: splittedSlug, // this is passed to the getStaticProps function
page: i, //this is not passed to the getStaticProps function
},
locale,
});
}
}
}
}
return {
paths: paths,
fallback: false,
};
}
Accessing the page query param in getStaticProps would solve the problem since I can pass that value to the API request and get the right blogposts to display on the right paginated pages.
Fetching data directly in the component is not preferable for SEO reasons since it will be client-side JS.
All the logic is for the whole site is in the pages/[[...slug.jsx]] file since there are multiple locales, but would it make sense to split it up so I have a dynamic file for the blog itself (across locales)?
I have tried returning the page query param in several different ways, but getStaticProps will only see the param that matches the filename (ex. params.slug will be accessible because the file is called [[...slug]].jsx].

Nested pages in Hygraph (GraphCMS) with Nextjs dynamic routing

I am trying to create nested pages using Hygraph (GraphCMS) and Nextjs. I thought about using [..slug].js
But the problem I have is that writing my slug-like this "page/subpage" the GraphCMS returns the following structure which is not an array or nested
{
"data": {
"pages": [
{
"slug": "page/subpage"
}
]
}
}
And if I try to get static paths Nextjs simply will find nothing at page/subpage, it will always be 400 not found because it is a string and only will work as http://localhost:3000/page%2Fsubpage
Does anyone know how to create subpages in GraphCMS (Hygraph)? Really need your help.
Right now I have the following code to get static paths
export async function getStaticPaths() {
const client = graphcmsClient()
const { pages } = await client.request(`
query {
pages(where: {slug_not_in: ["home"]}) {
slug
}
}
`)
return {
paths: pages.map(({ slug }) => ({
params: { slug },
})),
fallback: false
}
}

Flatten Apollo response from WpGraphql in next.js

I'm trying to change the result that apollo returns with the data on the client side. I've read in the docs of version 3 that maybe this is possible. I currently have a graphql query that returns data within an EDGES array, and then each item is in a "node" before you get to the actual data.
The query looks like:
export const QUERY_ALL_POSTS = gql`
query AllPosts {
posts(first: 20) {
__typename
edges {
__typename
node {
__typename
author {
node {
avatar {
height
url
width
}
id
name
slug
}
}
id
categories {
edges {
node {
databaseId
id
name
slug
}
}
}
content
date
excerpt
featuredImage {
node {
altText
caption
sourceUrl
srcSet
sizes
id
}
}
modified
databaseId
title
slug
isSticky
}
}
}
}
`;
And the response looks like:
I know that I can write the typePolicy this way to change the date string for example:
Post:{
fields:{
date(){
return 'date edit'
}
}
},
So I wanted to see if I could change the response of the entire array that gets returned to something like this because I'll be needing to check for edges and nodes in every component which is kinda of annoying:
posts:[
{...item 1},
{...item 2},
{...item 3},
...etc
]
I've tried to write a typePolicy like this, and it returns the correct data, but when I query it on the frontend, nothing has changed. The data is still contained in an Edges object and each item is still in a NODE from the wpGraphql backend.
typePolicies: {
Post:{
fields:{
date(){
return 'date edit test' // returns a modified date object
}
}
},
Query: {
fields: {
// posts: flatten(),
posts:{
read(posts, { readField }) {
if(posts){
const newItem = posts.edges.map(post => {
return post.node
})
console.log('newItem', newItem)
return newItem
}
return posts // does not return a modified posts object
},
}
},
},
},
Can I modify the response like this or do I just have to write a custom helper function to modify the data every time I query it before using the data inside a component?

How do I access data in GraphQL if not by the id?

I'm using GraphQL with Meteor and Pup v2, and I have a problem accessing the users data via a special ID provided to every user on signup, this ID will be used in a link (mysite.com/user/specialId) so other users can view the searched users account. Problem is, I can't get the data with the special ID, I can't get any data back if I don't pass in the users _id provided by MongoDB. Below I have a bunch of the code used:
Attempt 1
I tried to use a custom on-the-go way just to be able to at least access the data to see if it works (and then implement it correctly later)
const GET_USER_DETAILS = gql`
query user($userId: String) {
user(userId: $userId) {
userId
username
_id
}
}
`;
Here I export so I can get the data:
export default compose(
graphql(GET_USER_DETAILS, {
options: ({ match }) => ({
fetchPolicy: 'no-cache',
variables: {
// existing specialId for testing purposes, to be replaced with match.params.userId
userId: "J4xZzvvhBDSEufnBn",
},
}),
}),
)(PublicProfileView);
This returns a 400 error Network error: Response not successful: Received status code 400 error and after multiple attempts, I could not fix it, so I tried a different approach...
Attempt 2
I tried to go deep into the files and change the GraphQL. Created a new query:
query userById($userId: String) {
userById(userId: $userId) {
...UserAttributes
}
}
(Mentioned fragment)
fragment UserAttributes on User {
_id
name {
...
}
username
emailAddress
oAuthProvider
roles {
...
}
settings {
...
}
userId
}
Tried to add new item in API:
type Query {
...
userById(userId: String): User
...
}
Resolver:
resolvers: {
Query: {
...
userById: (parent, args) => {
// Assuming args equals an object like { _id: '123' };
return UserQueries.userById(args);
},
},
},
query.js, attempt 1:
userById: (parent) => queryUsers.find({ userId: parent.userId }, { sort: { createdAt: 1 } }).fetch()
Attempt 2:
userById: (parent, args, context) => {
return queryUsers({
userId: parent.userId,
});
},
And finally
Attempt 3
I tried to modify the get query
const getQueryModified = (options) => {
// console.log(options.userId)
try {
return options.userId
? { 'userId': options.userId }
: { userId: options.userId };
} catch (exception) {
throw new Error(`[queryUsers.getQuery] ${exception.message}`);
}
};
Here is the original query I tried to modify:
const getQuery = (options) => {
try {
return options.search
? {
_id: { $ne: options.currentUser._id },
$or: [
{ 'profile.name.first': options.search },
{ 'profile.name.last': options.search },
{ 'emails.address': options.search },
// { 'userId': options.search },
{ 'services.facebook.first_name': options.search },
{ 'services.facebook.last_name': options.search },
{ 'services.facebook.email': options.search },
],
}
: { _id: options.currentUser._id };
} catch (exception) {
throw new Error(`[queryUsers.getQuery] ${exception.message}`);
}
};
Unfortunately this was also unsuccessful, the best I get from these when executing the below query is null...
userById(userId: "J4xZzvvhBDSEufnBn"){
username
}
All I want is to get the user data from their userId and not their _id, but I can't seem to figure out how to do it

When querying for a single item in graphql, meteor, and apollo i get null values returned

I have probably overlooked something in the docs, but I have seem to run into a problem with being able to get a single object from my graphql queries.
Here is the schema:
type Query {
product(name: String!): Product
}
type Product {
_id: String
name: String
}
Here is the resolver:
Query: {
product (_, args) {
return Products.find({where: args})
},
products () {
return Products.find().fetch()
}
}
Here is the Query:
query {
product(name: "burgers") {
name
}
}
I get a result of this:
{
"data": {
"product": {
"name": null
}
}
}
Am I just forgetting to add something to this, and if so could you point me the right direction.
If Products is a Meteor Collection, then .find returns a cursor, so the right thing to return would be Products.findOne({name: args.name})
http://docs.meteor.com/api/collections.html#Mongo-Collection-findOne

Resources