I have sample React blog with dummy data, but when I host it on netlify the dynamic url routing doesnt work. It is designed so there is two js pages, one lists all posts (index) and other is detail page for post (single post). Because I use headless CMS the single post template is one for all and it changes the data depending on the slug selected, the single post page is "generated" depending on which slug you choose.
The build is successful and index works, you can also click on a post and it will be generated, but when you click on "next post" (to generate the same template with different data from slug), it doesnt recognizes the URL and shows error:
Page Not Found Looks like you've followed a broken link or entered a URL that doesn't exist on this site.
"going back" also doesnt work and same error persist. you can only go back to blog by entering the index URL in browser. from index you can access all posts when you click on them, but the dynamic routing from each post doest work.
# index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Route, Routes } from 'react-router-dom'; // dyanmic URL routing
import AllPosts from './pages/AllPosts';
import SinglePost from './pages/SinglePost';
import Footer from './Footer';
import Header from './Header';
// environment variables
require('dotenv').config();
// Use the createRoot API to render your app
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<React.StrictMode>
<BrowserRouter>
<Header />
<Routes> {/*AllPosts is basically index.html */}
<Route exact path="/" element={<AllPosts />} />
<Route path='/posts/:slug' element={<SinglePost />} />
</Routes>
<Footer />
</BrowserRouter>
</React.StrictMode>
);
# SinglePost.js
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
const graphqlToken = process.env.GRAPHQL_TOKEN;
const endpoint = graphqlToken;
const SinglePost = () => {
const { slug } = useParams(); // obtained from AllPost when clicking, slug is variable for dynamic rendering
const [post, setPost] = useState(null); // fetch post data
const [pcedges, setPcedges] = useState([]); // Create a state for PostConnect edges (next and prev posts)
const [currentPostIndex, setCurrentPostIndex] = useState(0); // Initialize currentPostIndex to 0
// Your GraphQL query, slug is variable when fetching
// Query also PostConnection for suggesting other posts.
const query = `query MyQuery($slug: String!) {
posts(where: {slug: $slug}) {
title
excerpt
id
date
slug
content {
markdown
}
coverImage {
url
}
author {
name
title
picture {
url
}
}
}
postsConnection (orderBy: date_DESC){
edges {
node {
title
slug
date
}
}
}
}
`;
const variables = { //this const 'variables' is not used because dynamic variable { slug } defined below is for dynamic content
"slug": slug,
};
const formatDate = (dateString) => { //Date formatting
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const formattedDate = new Date(dateString).toLocaleDateString(undefined, options);
return formattedDate;
};
// fetchData defined outside useEffect hook, for dynamic calling for every different slug
const fetchData = async (slug) => {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query,
variables: { slug }, // Pass the slug as the variable
}),
});
const responseData = await response.json();
const edges = responseData.data.posts; // define post data
const pcEdges = responseData.data.postsConnection.edges; // define next and prev posts from PostConnect (in query)
setPcedges(pcEdges);
// next and prev posts routing by setting the post slug
setCurrentPostIndex(pcEdges.findIndex((pcedge) => pcedge.node.slug === slug));
if (edges && edges.length > 0) {
setPost(edges[0]); //first value([0]) of query always is post data
} else {
setPost(null);
}
} catch (error) {
console.error('Error fetching data:', error);
setPost(null);
}
};
// React hook
useEffect(() => {
fetchData(slug); //call fetchData with post content depending on slug
// second arg for useEffect is slug to check for changes.
}, [slug]);
if (!post) {
return <div>Loading...</div>;
}
// set next and prev post; (orderBy: date_DESC) in graphQL query is necessary for this order to work, otherwise links are random
const nextPost = pcedges[currentPostIndex + 1]?.node;
const previousPost = pcedges[currentPostIndex -1]?.node;
//set slug in order for post content to fetch
const handleNextPostClick = () => {
if (currentPostIndex + 1 < pcedges.length) {
const nextPostSlug = pcedges[currentPostIndex + 1]?.node.slug;
if (nextPostSlug) {
fetchData(nextPostSlug);
}
}
};
const handlePreviousPostClick = () => {
if (currentPostIndex - 1 >= 0) {
const previousPostSlug = pcedges[currentPostIndex - 1]?.node.slug;
if (previousPostSlug) {
fetchData(previousPostSlug);
}
}
};
return (
<main class='post'>
<section class='post__header'>
<div class="formatedDate">
<dl>{formatDate(post.date)}</dl>
</div>
<h2>{post.title}</h2>
</section>
<section class='post__content'>
<div class='post__content__column'>
<div class="description">
<div class="detail">
<div class="img-container">
{/* if no picture from query use hardcoded placeholder */}
{post.author.picture && post.author.picture.url ? ( <img src={post.author.picture.url}/> ) : ( <img src='https://26159260.fs1.hubspotusercontent-eu1.net/hubfs/26159260/personalBlog/cv-big-photo2.png'/> )}
</div>
<div>
<p class='author'>{post.author.name}</p>
<p class='title'>{post.author.title}</p>
</div>
</div>
</div>
<hr />
<div class="footer">
<div class='footer__other-posts'>
{/* if there is no nextPost or prevPost, dont show link option */}
{nextPost && (
<div>
<p>NEXT POST</p>
<a href={`/posts/${nextPost.slug}`} onClick={handleNextPostClick}>{nextPost.title}</a>
</div>
)}
{previousPost && (
<div>
<p>PREVIOUS POST</p>
<a href={`/posts/${previousPost.slug}`} onClick={handlePreviousPostClick}>{previousPost.title}</a>
</div>
)}
</div>
<hr />
<div class='footer__backlink'>
<a href='/'>← Back to the blog</a>
</div>
</div>
</div>
<div className="post__content__post-content">
<div class='post__content__post-content__img-container'>
{/* if no picture from query use hardcoded placeholder */}
{post.coverImage && post.coverImage.url ? (<img id='post-img' src={post.coverImage.url} /> ) : ( <img id='post-img' src='https://26159260.fs1.hubspotusercontent-eu1.net/hubfs/26159260/personalBlog/cover-img2.jpg' />)}
</div>
<hr />
<ReactMarkdown>{post.content.markdown}</ReactMarkdown> {/* render MarkDown content (post body) */}
</div>
</section>
</main>
);
};
export default SinglePost;
// AllPosts.js
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Link } from 'react-router-dom';
// Access the GraphQL token using the environment variable
const graphqlToken = process.env.GRAPHQL_TOKEN;
// GraphQL endpoint URL
const endpoint = graphqlToken;
const AllPosts = () => {
// get title and ID when clicking on a post (see below)
const handleClick = (id, title) => {
console.log('Title:', title);
console.log('ID:', id);
};
const [data, setData] = useState([]);
// Your GraphQL query and variables
const query = `query MyQuery {
posts (orderBy: date_DESC){
title
excerpt
id
date
slug
}
}`;
const variables = {
// "first": 25,
};
// format date from 15.07.2020 to weekday, Month dayNum, year.
const formatDate = (dateString) => {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const formattedDate = new Date(dateString).toLocaleDateString(undefined, options);
return formattedDate;
};
// React hook
useEffect(() => {
// fetchData: asynchronous function to fetch data
async function fetchData() {
try {
// fetch: send POST request to endpoint
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
// include query and variables
query,
variables,
}),
});
// Response from server is parsed as JSON
const responseData = await response.json();
// assuming response is object with data, page property and edge array
const edges = responseData.data.posts;
// console.log(edges)
// new response state
setData(edges);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData();
// By passing an empty dependency array (second argument of useEffect),
// you tell React not to watch for any changes in specific dependencies
// and only run the effect once.
// For example see dynamic routing depending on slug (see SinglePost.js )
}, []); // Run the effect only once on component mount
return (
// rendering the fetched data
<main> {/* top-level container */}
{/* Display the fetched data here */}
<section class="title">
<h1>Latest</h1>
<p>Our latest blog posts.</p>
</section>
<section>
<ul>
{data.map((edge, index) => (
<li key={index} onClick={() => handleClick(edge.id, edge.title)}> {/* handle click for post title and ID*/}
<div class="article">
<div class="formatedDate">
<dl>{formatDate(edge.date)}</dl> {/* formatted date */}
</div>
<div class="article__text">
<div>
<h2><Link to={`/posts/${edge.slug}`}>{edge.title}</Link></h2> {/* use Link for dynamic routing */}
<p>{edge.excerpt}</p>
</div>
<span><a href={`/posts/${edge.slug}`}>Read more →</a></span>
</div>
</div>
</li>
))}
</ul>
</section>
</main>
);
};
export default AllPosts;
this config works in localhost
I think the issue may be in SinglePost.js:
// React hook
useEffect(() => {
fetchData(slug); //call fetchData with post content depending on slug
// second arg for useEffect is slug to check for changes.
}, [slug]);
or with the const variables from my query:
const variables = { //this const 'variables' is not used because dynamic variable { slug } defined below is for dynamic content
"slug": slug,
};
react-router-dom
uses Link
component for SPA routing. SPA routing means your app only has 1 single HTML file, routing should be done with JS by handling states, values in the memory (in React it stored on the virtual dom). In your code, you are using <a>
(anchor tag) which will change the window.location
, load the whole new page, and the React virtual dom gets cleared meaning all data will be lost.
<a href={`/posts/${nextPost.slug}`} onClick={handleNextPostClick}>{nextPost.title}</a>
You might want to try to replace a
tag with Link
element.