我有带有虚拟数据的示例 React 博客,但是当我将其托管在 netlify 上时,动态 url 路由不起作用。它的设计有两个js页面,一个列出了所有帖子(索引),另一个是帖子的详细信息页面(单个帖子)。因为我使用无头 CMS,所以单个帖子模板是通用的,它根据所选的 slug 更改数据,根据您选择的 slug “生成”单个帖子页面。
构建成功并且索引有效,您也可以单击帖子,它将生成,但是当您单击“下一篇帖子”(使用与 slug 不同的数据生成相同的模板)时,它无法识别 URL 并且显示错误:
找不到页面 您似乎访问了损坏的链接或输入了此网站上不存在的 URL。
“返回”也不起作用,并且同样的错误仍然存在。您只能通过在浏览器中输入索引 URL 来返回博客。从索引中,您可以在单击所有帖子时访问它们,但每个帖子的动态路由不起作用。
# 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;
此配置适用于本地主机
我认为问题可能出在 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]);
或者使用我的查询中的 const 变量:
const variables = { //this const 'variables' is not used because dynamic variable { slug } defined below is for dynamic content
"slug": slug,
};
react-router-dom
使用 Link
组件进行 SPA 路由。 SPA 路由意味着你的应用程序只有 1 个 HTML 文件,路由应该通过 JS 处理状态、内存中的值(在 React 中存储在虚拟 dom 上)来完成。在您的代码中,您使用 <a>
(锚标记),这将更改 window.location
,加载整个新页面,并且 React 虚拟 dom 被清除,这意味着所有数据都将丢失。
<a href={`/posts/${nextPost.slug}`} onClick={handleNextPostClick}>{nextPost.title}</a>
您可能想尝试用
a
元素替换 Link
标签。