Nuxt js full static dynamic page, payload undefined - static

I'm trying to generate page dynamically. After running nuxt generate the page's are successfully created. But loading the page in (with nuxt start) it says that the title is undefined which I send in the payload while generating.
The payload.js exists for every generated page but it won't work for some reason.
Page result:
nuxt.config.js
export default {
target: 'static',
generate: {
routes() {
const routing = [{url: 'page1', title: 'Page 1'}, {url: 'page2', title: 'Page 2'}, {
url: 'page3',
title: 'Page 3'
}];
return routing.map(route => {
return {
route: route.url,
payload: route,
};
});
}
}
}
pages/_.vue
<template>
<h1>{{payload.title}}</h1>
</template>
<script>
export default {
async asyncData({payload}) {
return {payload: payload}
}
}
</script>
commands:
nuxt generate
nuxt start

You will need to run nuxt generate then nuxt serve in order to test the generated routes.

Related

Gatsby server side rendering with getServerData() working locally but not on Netlify

I have an example site built with Gatsby 4.25.0, Node 14.20.1 and React 18.1.0 and served on Netlify. There are two pages- /test.js and a dynamic page /dogs/[breed].js. The test page fetches a list of dog breeds from an API and renders them in a list of links:
// test.js
<ul>
{dogBreeds.map((breed, idx) => (
<li key={idx}>
<Link to={`/dogs/${breed}`}>{breed}</Link>
</li>
))}
</ul>
The [breed].js page uses getServerData() to fetch an image of the breed passed to it from params and display it on the page:
export async function getServerData(context) {
const { breed } = context.params
try {
const res = await fetch(`https://dog.ceo/api/breed/${breed}/images/random`)
const data = await res.json()
console.log(`data from the getServerData function: ${data}`)
if (!res.ok) {
throw new Error(`Response failed`)
}
return {
props: {
breed,
imageUrl: data.message,
},
}
} catch (error) {
return {
status: 200,
headers: {},
props: {},
}
}
}
Running locally, this all works fine. I can navigate to the dynamic route from the links and the images show up fine. On Netlify, hovering over the links in test.js causes 404 errors to show up in the console from "loader.js" and "prefetch.js":
And clicking a dog breed link on /test leads me to a 404. We are using gatsby-plugin-netlify and #netlify/plugin-gatsby.

Next JS - how to give object a unique id?

I am trying to give my object a unique id by using Math.random, but I am getting the error below. If I change the Math.random to a integer like 4, the error goes away but I need the id to be unique.
Unhandled Runtime Error
Error: Text content does not match server-rendered HTML.
See more info here: https://nextjs.org/docs/messages/react-hydration-error
const data = [
{
id: Math.random(),
sentence: 'hello there',
},
{
id: Math.random(),
sentence: 'bye!',
},
]
export default function Index(props) {
return (
<div>
<h1>{data[0].id}</h1>
<h1>{data[0].sentence}</h1>
<h1>{data[1].id}</h1>
<h1>{data[1].sentence}</h1>
</div>
)
}
As explained in my comment and one of the answers, the problem is happening because Next.js pre-renders pages, therefore, the random number generated by Math.random() when pre-rendering the page on the server doesn't match the random number generated client-side when hydration occurs.
I'm not quite sure what you're trying to achieve by setting random ids to what seems to be "dummy data" (You could do it manually with constant values that will match both server and client-side) but I understand that this might be a simplified example.
You have a couple of options, typically you'd want to move any random generation code/logic inside a useEffect hook so it executes on the client-side only.
Another solution would be to move your "dummy data" and the rendering of this data to a separate component, let's call it DummyComponent:
const data = [
{
id: Math.random(),
sentence: 'hello there',
},
{
id: Math.random(),
sentence: 'bye!',
},
]
const DummyComponent = () => (
<>
<h1>{data[0].id}</h1>
<h1>{data[0].sentence</h1>
<h1>{data[1].id}</h1>
<h1>{data[1].sentence}</h1>
</>
)
export default DummyComponent
And import it dynamically on your page disabling ssr:
import dynamic from 'next/dynamic'
const DummyComponent = dynamic(() => import('../components/DummyComponent'), {
ssr: false,
})
export default function Index(props) {
return (
<div>
<DummyComponent />
</div>
)
}
Use the uuid npm package to generate unique id's.
import { v4 } from "uuid";
const data = [
{
id: v4(),
sentence: 'hello there',
},
{
id: v4(),
sentence: 'bye!',
},
]
First, the page is rendered on the server, then returned to the client to rehydrate, two times initializing data would cause different ID values. This is called hydration mismatch, which is solved by useId in React 18
But using NextJS, you could solve this problem by initializing data on server to keep it consistent
export default function Index(props) {
return (
<div>
<h1>{props.data[0].id}</h1>
<h1>{props.data[0].sentence}</h1>
<h1>{props.data[1].id}</h1>
<h1>{props.data[1].sentence}</h1>
</div>
);
}
export async function getServerSideProps(context) {
return {
props: {
data: [
{
id: Math.random(),
sentence: 'hello there',
},
{
id: Math.random(),
sentence: 'bye!z',
},
],
}, // will be passed to the page component as props
};
}
Stackblitz demo
References
getServerSideProps

"article seed # 0" => "article-seed-0" to navigate to other Page with that url?

I am routing dynamically with the compilation of static pages using gatsby-node.
const path = require('path');
const slugify = require('slugify');
const templateForContentType = {
articles: 'src/templates/ArticleDetail.tsx',
};
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
.
.
.
result.data.allStrapiFinancialEducationContents.nodes.forEach((node) => {
const contentType = contentTypes[node.content[0].strapi_component];
createPage({
path: `/articles/${slugify(node.title, {
lower: true,
})}`,
component: path.resolve(templateForContentType),
context: {
id: node.id,
},
});
});
};
gatsby compiles that statics files with the .title property
[
{title:"article-seed-0",
{title:"article-seed-1"
]
article-seed-0
...
article-seed-n
when I try to navigate to another page, it forms a url like this:
const {title:"article seed # 0" }
<Link to={article.title}>
this is the result:
Article%20Seed%20#0
the data comes to backend, is there any practical way for any kind of title to a url convention that can be associated with the way gatsby compiles these static files?
Gatsby compiles the static files with the given path:
path: `/articles/${slugify(node.title, {lower: true,})}`,
So, if you are using slugify to convert the title (node.title) to a valid path, it has nothing to do with Gatsby but slugify, which is removing the # to add a hyphen (-) in between among other things (indeed, it's creating a slug from a given string).
If your Link doesn't have the right target URL, just use slugify in the same way:
<Link to={slugify(article.title, {lower:true})}>

Gatsby fetching Wordpress Custom Post Types and create pages

I read the documentation and tried several tutorials, but I am stuck on fetching custom post types with GatsbyJS.
I tried several approaches, but none of them are working as expected. I always receive a 404.
This is a part of the snippet I am using, which works fine with pages and posts, but not with a custom post type.
The projects pages should be created under a project subfolder/path. Like: example.com/project/my-first-project
The part of the gatsby-node.js looks like that:
const createSinglePages = async ({ posts, gatsbyUtilities }) =>
Promise.all(
posts.map(({ previous, post, next }) =>
// createPage is an action passed to createPages
// See https://www.gatsbyjs.com/docs/actions#createPage for more info
gatsbyUtilities.actions.createPage({
// Use the WordPress uri as the Gatsby page path
// This is a good idea so that internal links and menus work 👍
path: post.uri,
// use the blog post template as the page component
component: path.resolve(
`./src/templates/${post.__typename.replace(`Wp`, ``)}.js`
),
// `context` is available in the template as a prop and
// as a variable in GraphQL.
context: {
// we need to add the post id here
// so our blog post template knows which blog post
// the current page is (when you open it in a browser)
id: post.id,
// We also use the next and previous id's to query them and add links!
previousPostId: previous ? previous.id : null,
nextPostId: next ? next.id : null,
},
})
)
);
The src/template/project.js file looks like this:
import React from "react";
import { Link, graphql } from "gatsby";
import Image from "gatsby-image";
import parse from "html-react-parser";
import Layout from "../components/Layout";
import Seo from "../components/Seo";
const ProjectTemplate = ({ data: { post } }) => {
const featuredImage = {
fluid: post.featuredImage?.node?.localFile?.childImageSharp?.fluid,
alt: post.featuredImage?.node?.alt || ``,
};
return (
<Layout>
<Seo title={post.title} description={post.excerpt} />
<article
className="blog-post"
itemScope
itemType="http://schema.org/Article"
>
<header>
<h1 itemProp="headline">{parse(post.title)}</h1>
<p>{post.date}</p>
{/* if we have a featured image for this post let's display it */}
{featuredImage?.fluid && (
<Image
fluid={featuredImage.fluid}
alt={featuredImage.alt}
style={{ marginBottom: 50 }}
/>
)}
</header>
{!!post.content && (
<section itemProp="articleBody">{parse(post.content)}</section>
)}
</article>
</Layout>
);
};
export default ProjectTemplate;
export const pageQuery = graphql`
query ProjectById(
# these variables are passed in via createPage.pageContext in gatsby-node.js
$id: String!
) {
# selecting the current post by id
post: wpProject(id: { eq: $id }) {
id
content
title
date(formatString: "MMMM DD, YYYY")
featuredImage {
node {
altText
localFile {
childImageSharp {
fluid(maxWidth: 1000, quality: 100) {
...GatsbyImageSharpFluid_tracedSVG
}
}
}
}
}
}
}
`;
Is the Gatsby API creating a subfolder automatically, or do I need to define that somewhere for each post type?
Any help appreciated!
You define the "subfolder" under the path field in:
gatsbyUtilities.actions.createPage({
path: post.uri,
component: path.resolve(
`./src/templates/${post.__typename.replace(`Wp`, ``)}.js`
),
context: {
id: post.id,
previousPostId: previous ? previous.id : null,
nextPostId: next ? next.id : null,
},
})
You just need to do something like:
path: `projects/${post.id}`
Check the slashes and trailing slashes here.
You cna replace projects for your dynamic project type if you fetch that information for a more automatic approach (assuming it's post.__typename).
In order to use Custom Post Types with WPGraphQL, you must configure the Post Type to show_in_graphql using the following field:
show_in_graphql : true
While Registering a new Custom Post Type
This is an example of registering a new "docs" post_type and enabling GraphQL Support.
add_action( 'init', function() {
register_post_type( 'docs', [
'show_ui' => true,
'labels' => [
//#see https://developer.wordpress.org/themes/functionality/internationalization/
'menu_name' => __( 'Docs', 'your-textdomain' ),
],
'show_in_graphql' => true,
'hierarchical' => true,
'graphql_single_name' => 'document',
'graphql_plural_name' => 'documents',
] );
} );

Electrode doesn't show dynamic data in page source

Using electrode, I noticed this weird behaviour -
When I view the page source after the page fully loads with all the api calls and data, I am only able to view the content that is static for example, the hyper links, headings, footer links etc.
I have created a custom token handler which checks the context object and populates the custom tokens present in the index.html.
So, whenever, I console.log(context.user.content), only the data that is static such as hyperlinks, headings, footer links are logged.
I guess this is the problem but I am not able to wrap my head around as to why electrode doesn't recognise the content being rendered dynamically.
Token-Handler.js file
import Helmet from 'react-helmet';
const emptyTitleRegex = /<title[^>]*><\/title>/;
module.exports = function setup(options) {
// console.log({ options });
return {
INITIALIZE: context => {
context.user.helmet = Helmet.renderStatic();
},
PAGE_TITLE: context => {
const helmet = context.user.helmet;
const helmetTitleScript = helmet.title.toString();
const helmetTitleEmpty = helmetTitleScript.match(emptyTitleRegex);
return helmetTitleEmpty ? `<title>${options.routeOptions.pageTitle}</title>` : helmetTitleScript;
},
REACT_HELMET_SCRIPTS: context => {
const scriptsFromHelmet = ["link", "style", "script", "noscript"]
.map(tagName => context.user.helmet[tagName].toString())
.join("");
return `<!--scripts from helmet-->${scriptsFromHelmet}`;
},
META_TAGS: context => {
console.log(context,'123') //this is where I am checking
return context.user.helmet.meta.toString();
}
};
};
default.js
module.exports = {
port: portFromEnv() || "3000",
webapp: {
module: "electrode-react-webapp/lib/express",
options: {
prodBundleBase: '/buy-used-car/js/',
insertTokenIds: false,
htmlFile: "./{{env.APP_SRC_DIR}}/client/index.html",
paths: {
"*": {
content: {
module: "./{{env.APP_SRC_DIR}}/server/views/index-view"
},
}
},
serverSideRendering: true,
tokenHandler: "./{{env.APP_SRC_DIR}}/server/token-handler"
}
}
};
Any clue anyone?
EDIT 1
However, any following updates that occur on the meta tags are rendered. I'm not sure that is something electrode allows or is a feature of react-helmet.
EDIT 2
SSR is enabled in electrode.
After digging in the docs, realised that there was a slight misunderstanding. So, if data needs to be present in the page source, it needs to be pre-rendered by the server.
Why it wasn't showing at the time I asked the question? Because, data was being evaluated at run-time due ot which the page source only rendered the static content.
Electrode already provides an abstraction, each component that is being rendered has an option to load with pre-fetched data. The catch here is, you have to evaluate what all data needs to be present at runtime because more data is directly proportional to page loading time (as the server won't resolve unless the api you are depending on returns you with either a success or failure )
In terms of implementation, each route has a parameter called init-top which is executed before your page loads.
const routes = [
{
path: "/",
component: withRouter(Root),
init: "./init-top",
routes: [
{
path: "/",
exact: true,
component: Home,
init: "./init-home"
},
in init-home, you can define it something on the lines of -
import reducer from "../../client/reducers";
const initNumber = async () => {
const value = await new Promise(resolve => setTimeout(() => resolve(123), 2000));
return { value };
};
export default async function initTop() {
return {
reducer,
initialState: {
checkBox: { checked: false },
number: await initNumber(),
username: { value: "" },
textarea: { value: "" },
selectedOption: { value: "0-13" }
}
};
}
So,now whenever you load the component, it is loaded with this initialState returned in init-home
I'll just post it here, in case anyone is stuck.

Resources