NextJs: Static export with dynamic routes - next.js

I am a bit confused by the documentation and not sure if it's possible what I am trying to do.
Goal:
Export NextJS app statically and host it on netlify
Allow users to create posts and have links to these posts which work
For example:
User creates a new post with the id: 2
This post should be publicly accessible under mysite.com/posts/2
I'd imagine that I can create a skeleton html file called posts.html and netlify redirects all posts/<id> requests to that posts.html file which will then show the skeleton and load the necessary data via an API on the fly.
I think without this netlify hack, my goal of static export + working links to dynamic routes is not possible with next.js according to their documentation since fallback: true is only possible when using SSR.
Question: How can I achieve my dream setup of static nextjs export + working links to dynamic routes?
EDIT:
I just found out about Redirects. They could be the solution to my problem.

getStaticProps and getStaticPaths()
It looks like using getStaticProps and getStaticPaths() is the way to go.
I have something like this in my [post].js file:
const Post = ({ pageContent }) => {
// ...
}
export default Post;
export async function getStaticProps({ params: { post } }) {
const [pageContent] = await Promise.all([getBlogPostContent(post)]);
return { props: { pageContent } };
}
export async function getStaticPaths() {
const [posts] = await Promise.all([getAllBlogPostEntries()]);
const paths = posts.entries.map((c) => {
return { params: { post: c.route } }; // Route is something like "this-is-my-post"
});
return {
paths,
fallback: false,
};
}
In my case, I query Contentful using my getAllBlogPostEntries for the blog entries. That creates the files, something like this-is-my-post.html. getBlogPostContent(post) will grab the content for the specific file.
export async function getAllBlogPostEntries() {
const posts = await client.getEntries({
content_type: 'blogPost',
order: 'fields.date',
});
return posts;
}
export async function getBlogPostContent(route) {
const post = await client.getEntries({
content_type: 'blogPost',
'fields.route': route,
});
return post;
}
When I do an npm run export it creates a file for each blog post...
info - Collecting page data ...[
{
params: { post: 'my-first-post' }
},
{
params: { post: 'another-post' }
},
In your case the route would just be 1, 2, 3, etc.
Outdated Method - Run a Query in next.config.js
If you are looking to create a static site you would need to query the posts ahead of time, before the next export.
Here is an example using Contentful which you might have set up with blog posts:
First create a page under pages/blog/[post].js.
Next can use an exportMap inside next.config.js.
// next.config.js
const contentful = require('contentful');
// Connects to Contentful
const contentfulClient = async () => {
const client = await contentful.createClient({
space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID,
accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
});
return client;
};
// Gets all of the blog posts
const getBlogPostEntries = async (client) => {
const entries = await client.getEntries({
content_type: 'blogPost',
order: 'fields.date',
});
return entries;
};
module.exports = {
async exportPathMap() {
const routes = {
'/': { page: '/' }, // Index page
'/blog/index': { page: '/blog' }, // Blog page
};
const client = await contentfulClient();
const posts = await getBlogPostEntries(client);
// See explanation below
posts.items.forEach((item) => {
routes[`/blog/${item.fields.route}`] = { page: '/blog/[post]' };
});
return routes;
},
};
Just above return routes; I'm connecting to Contentful, and grabbing all of the blog posts. In this case each post has a value I've defined called route. I've given every piece of content a route value, something like this-is-my-first-post and just-started-blogging. In the end, the route object looks something like this:
routes = {
'/': { page: '/' }, // Index page
'/blog/index': { page: '/blog' }, // Blog page
'/blog/this-is-my-first-post': { page: '/blog/[post]' },
'/blog/just-started-blogging': { page: '/blog/[post]' },
};
Your export in the out/ directory will be:
out/
/index.html
/blog/index.html
/blog/this-is-my-first-post.html
/blog/just-started-blogging.html
In your case, if you are using post id numbers, you would have to fetch the blog posts and do something like:
const posts = await getAllPosts();
posts.forEach((post) => {
routes[`/blog/${post.id}`] = { page: '/blog/[post]' };
});
// Routes end up like
// routes = {
// '/': { page: '/' }, // Index page
// '/blog/index': { page: '/blog' }, // Blog page
// '/blog/1': { page: '/blog/[post]' },
// '/blog/2': { page: '/blog/[post]' },
// };
The next step would be to create some sort of hook on Netlify to trigger a static site build when the user creates content.
Also here is and idea of what your pages/blog/[post].js would look like.
import Head from 'next/head';
export async function getBlogPostContent(route) {
const post = await client.getEntries({
content_type: 'blogPost',
'fields.route': route,
});
return post;
}
const Post = (props) => {
const { title, content } = props;
return (
<>
<Head>
<title>{title}</title>
</Head>
{content}
</>
);
};
Post.getInitialProps = async ({ asPath }) => {
// asPath is something like `/blog/this-is-my-first-post`
const pageContent = await getBlogPostContent(asPath.replace('/blog/', ''));
const { items } = pageContent;
const { title, content } = items[0].fields;
return { title, content };
};
export default Post;

Related

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.

How do you do server side rendering with nextjs [id].js in headless wordpress? fetch single page using graphql from Wordpress. like service/[id].js

I have a nextjs project that is using apollo graphql to fetch data from the backend. I am trying to render my page using server side rendering. But I am currently using graphql apollo hooks to fetch my data from the backend, and the react hooks prevents me from calling my backend inside of the getServerSideProps.
Create and fetch single page using graphql from Wordpress with clean URLs like services/[id].js
N.B: Warning Show ( Error: Response not successful: Received status code 500)
import {
gql,
ApolloClient,
InMemoryCache
} from "#apollo/client";
export const client = new ApolloClient({
uri: 'https://.........../graphql',
cache: new InMemoryCache()
});
const serviceDetail = (serviceOutput) => {
return (
<div>
{serviceOutput.serviceTitle}
{serviceOutput.serviceContent}
</div>
)
}
export const getServerSideProps = async (context) => {
const result = await client.query({
query: gql`
query serData($id: id!) {
HomePage: pageBy(uri: "https://......./home/") {
aboutSection {
serviceSec(id: $id) {
id
serviceTitle
serviceContent
serviceImage {
sourceUrl
}
}
}
}
}
`,
variables: {
id: context.params.id
}
})
return {
props: {
serviceOutput: result.data.HomePage.aboutSection.serviceSec;
},
};
}
export default serviceDetail;
i am not an expert, but as far i have used. you cannot use Apollo together with next js fetching method(ssg,ssr,isr).
Apollo runs queries on client side, and can be used with useQuery and useLazyQuery. while next js fetching is completely different.
I will demonstrate 2 ways here.
-- Using Apollo --
const FETCH_ALL = gql`
query MyQuery($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
title
}
}
}
}
`;
export default function LoadMoreList() {
const { data } = useQuery(FETCH_ALL, {
variables: { first: 5, after: null },
notifyOnNetworkStatusChange: true,
});
return (
<>
<div>
{postdata.map((node, index) => {
{
return (
<div key={index}>
<h1>{node?.node?.title}</h1>
</div>
);
}
})}
</div>
</>
)}
=== using fetch and getStaticProps ==
--File1 (this is a fetch function, to which you pass your queries and variables)
async function fetchAPI(query, { variables } = {}) {
const headers = { "Content-Type": "application/json" };
const res = await fetch(process.env.WP_API, {
method: "POST",
headers,
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors) {
console.log(json.errors);
throw new Error("Failed to fetch API");
}
return json.data;
}
export default fetchAPI;
-- File2 (this is a file that contains your query)
import fetchAPI from "./fetching";
export async function homeheadposts() {
const data = await fetchAPI(
`
query homeheadposts {
posts(first: 7) {
edges {
node {
id
slug
title
featuredImage {
node {
sourceUrl
}
}
excerpt(format: RAW)
}
}
}
}
`
);
return data?.posts;
}
-- File3 (place this function , where you wanna call and use the data, )
export async function getStaticProps() {
const latestPosts = await homeheadposts();
return {
props: { latestPosts },
};
}
export default function CallingData({ latestPosts }) {
console.log(latestPosts);
return <h1>hello</h1>;
}

Next.js getStaticProps/getServerSideProps best use case

I have a blog that is using getStaticProps + getStaticPaths to generate pages from headless WP - all good. Then I have a component that is connecting to the database to allow me to link to the next post in any within any individual category. This component is then used inside the ...postSlug page.
So basically the component works perfectly if I switch to using getServerSideProps for posts, as it updates the 'previous' + 'next' URLs as each post is requested and built on the server. However, if I use getStaticProps the component doesn't update with the correct URLs unless I manually refresh the page. So I need to use some client-side data fetching within the component, with useState, useEffect or SWR or something - but I'm not sure about the best way to do it (or even if that would work or if I should just use gServerSideProps).
Any help would be greatly appreciated. Thanks! Component below edited for clarity ...
export default function MovePost() {
const { usePost } = client
const post = usePost()
const { query = {} } = useRouter()
const { categorySlug } = query
const currentPaginationCursor = btoa( `arrayconnection:${post.databaseId}` )
const { posts } = client.useQuery()
const previous = posts({
first: 1,
before: currentPaginationCursor,
where: { categoryName: `${categorySlug}` },
}).nodes[0]?.slug
return (
<>
{ previous &&
<Link href={`/archive/${categorySlug}/${previous}`}>
<a ...
export default function Page() {
const { usePost } = client
const post = usePost()
return (
<>
<Layout>
<div dangerouslySetInnerHTML={{ __html: post?.content() ?? '' }} />
<MovePost />
</Layout>
</>
)
}
export async function getStaticProps(context: GetStaticPropsContext) {
return getNextStaticProps(context, {
Page,
client,
notFound: await is404(context, { client }),
})
}
export function getStaticPaths() {
return {
paths: [],
fallback: 'blocking',
}
}

Dynamic Content not loading in NextJs Dynamic Pages

Currently I have a Blogs collection type in my Strapi CMS with an Id and title data fields. I'm using NextJs for my frontend to dynamically load blog content for each blog page. But my content doesn't load when my dynamic page is loaded.
Page where individual blogs are stored:
{posts &&
posts.map((item, idx) => (
<Link href={`/BlogPage/${item.id}`}>
<div>
<img src={`http://localhost:1337${item.Thumbnail.url}`}/>
</div>
</Link>
Then inside my BlogPage directory i have a file [id].js:
export default function name({blog}) {
return (
<>
<div>
{blog.Title}
</div>
</>
)}
// Tell nextjs how many pages are there
export async function getStaticPaths() {
const res = await fetch("http://localhost:1337/blogs");
const posts = await res.json();
const paths = posts.map((blog) => ({
params: { id: blog.id.toString() },
}));
return {
paths,
fallback: false,
};
}
// Get data for each individual page
export async function getStaticProps({ params }) {
const { id } = params;
const res = await fetch(`http://localhost:1337/blogs?id=${id}`);
const data = await res.json();
const posts = data[0];
return {
props: { posts },
};
}
This takes me to this URL http://localhost:3000/BlogPage/1 and gives me an error
TypeError: Cannot read property 'Title' of undefined
Try to out the getStaticProps and getStaticPaths of name function
export async function getStaticPaths() {
const res = await fetch("http://localhost:1337/blogs");
const posts = await res.json();
const paths = posts.map((blog) => ({
params: { id: blog.id.toString() },
}));
return {
paths,
fallback: false,
};
}
export async function getStaticProps({ params }) {
const { id } = params;
const res = await fetch(`http://localhost:1337/blogs?id=${id}`);
const data = await res.json();
const posts = data[0];
return {
props: { posts },
};
}
export default function name({posts }) { // change this line
return (
<>
<div>
{posts.Title} // change this line // Are you sure is it Title? not title? if it is with lowercase, it will return null
</div>
</>
)
}

Error: A required parameter (slug) was not provided as a string in getStaticPaths for /posts/[slug]

I have the following [slug].js file in my project:
import Head from "next/head";
import PostContent from "../../components/posts/post-detail/post-content";
import { getPostByName, getAllPosts } from "../../helpers/api-util";
function PostDetailPage(props) {
const post = props.selectedPost;
console.log(post);
if (!post) {
return (
<div className="">
<p>Loading...</p>
</div>
);
}
return <PostContent post={post.slug} />;
}
export async function getStaticProps(context) {
const blogSlug = context.params.slug;
const post = await getPostByName(blogSlug);
return {
props: {
selectedPost: post,
}, // will be passed to the page component as props
};
}
export async function getStaticPaths() {
const posts = await getAllPosts();
const paths = posts.map(post => ({ params: { blogSlug: post.slug } }));
return {
paths: paths,
fallback: "blocking",
};
}
export default PostDetailPage;
This is my file structure:
I am getting my data from firebase with the following data structure:
The idea is that when I click my post on the 'all posts' page, I get into the PostContent component that contains all my post info.
Once I try to click on a particular post, I am getting the error mentioned in the subject.
Slug is not a string so I am not entirely sure why I am getting this.
Thanks
You have mismatch between filename dynamic key and what you expect in the code.
You return blogSlug key in getStaticPaths:
const paths = posts.map(post => ({ params: { blogSlug: post.slug } }));
but your file is named [slug].js and you expect a slug key here in getStaticProps:
const blogSlug = context.params.slug;
It should be consistent, in this case it should be named slug everywhere.

Resources