How to Manage a Navigation Menu in NextJS with WordPress - wordpress

I'm building a NextJS app using headless WordPress with GraphQL. It's not clear from the documentation where I should be calling the query to create the site navigation.
https://github.com/lfades/next.js/tree/examples/cms-wordpress/examples/cms-wordpress
The navigation is controlled dynamically by WordPress Menus (Appearance > Menus) on the backend and I can successfully access these menuItems via GraphQL without any issue on the index.js and posts/[slug].js page templates in Next JS.
// index.js
export default function Index({ primaryMenu = [] }) {
return (
<Layout>
<Header>
{primaryMenu.map((item, index) => {
return (<a href={item.url}>{item.label}</a>)
)}
</Header>
</Layout>
);
}
export async function getStaticProps() {
const primaryMenu = await getPrimaryMenu(); // Get menu via GraphQL
return {
props: { primaryMenu },
};
}
The issue I'm having with this is I am repeating the getStaticProps function on each template and I should be able to use some sort of global query for this, either in the <header/> component itself or another method. I'm unable to find documentation on how to do this and it doesn't work in components.
Any guidance (or examples) on where a global query such as a dynamic Navigation query would live in a NextJS app is appreciated.

There are a couple of ways you can do it:
You can menuItems query with useQuery() from #apollo/client inside the Layout component so that its available to all pages which are wrapped inside the Layout. However the problem with this is that, there will be a load time and the data won't be prefetched and readily available like we can do with getServerSideProps() ( at page level ). Because this will be at component level.
import { useQuery } from "#apollo/client";
export default function Layout () {
const { loading, data } = useQuery( GET_MENU_QUERY )
return {...}
}
You can use swr that uses caching strategy. There is blog that explains how to use it

I battled this for a while (for JD site) with redux and wp rest, but I think theory should be the same for gql + apollo client.
You need to override Next App _app with a custom class that extends App.
And you might need to inject an instance of apollo client into AppContext using a HOC. I used this wrapper for Redux. Would need to be modelled after that.
Edit: (Looks like someone has made it already)
// export default withRedux(makeStore)(MyApp);
export default withApollo(apolloClient)(MyApp); ???
Then in your App getInitialProps, you can make query to get menu. By default apollo client query will grab cached value if it's in the cache store already I believe.
static async getInitialProps(appContext) {
const { isServer, pathname, apollo? } = appContext.ctx;
// do menu query
const menu = apollo.query???
// Redux version
// const state = store.getState();
// let main_menu = state.menu;
// if (!state.menu) {
// const menu = await apiService().getMenu("main");
// main_menu = menu;
// store.dispatch({ type: "SET_MENU", payload: menu });
// }
...
// call the page's `getInitialProps` and fills `appProps.pageProps`
const initialProps = await App.getInitialProps(appContext);
const appProps: any = {
...initialProps,
menu: main_menu
};
return appProps;
}
Now menu is in the page props of the App Component, which can be passed down.
Or you can use apollo client to make the query again in a child component. So when you make the query again, in header or whatever, it will take the cached response provided it's the same query.
I made an endpoint for menus that included the template name + post slug along with the menu items and mapped the wp templates to next routes.
const menu = useSelector((state: any) => state.menu);
const menuItems = menu.map((item: any) => {
const path = getTemplatePath(item.template);
return (
<Link key={item.slug} href={`/${path}`} as={`/${item.slug}`} scroll={false}>
<a>{item.title}</a>
</Link>
);
});

Related

nextjs reload page with Link component

I have a simple nextjs 13 application (appDir) with 2 pages and a <Link/> component navigation.
first page Home - static
second page Test - receiving dynamic random content on the server side (fetch) from a third-party source.
Problem: When the application is rendered everything works fine, but when I try to switch between pages, my test page shows the old content, I can refresh the browser to get the actual data, is very similar to navigating through regular links <a/>, but i need without reloading the entire application.
Q: How can I force nextjs 13 to reload the Test page when I switch between pages with <Link/> component?
// src/components/navbar.tsx
'use client'
import {usePathname} from "next/navigation";
import Link from "next/link";
const navItems = [
{text: 'Home', href: '/'},
{text: 'Test', href: '/test'}
];
const Navbar = () => {
const pathname = usePathname();
return <nav className="nav nav-masthead justify-content-center float-md-end">
{navItems.map((item: { text: string, href: string, link?: boolean }, idx: number) => (
<Link key={idx} href={item.href} className={`nav-link${item.href === pathname ? ' active' : ''}`}>
{item.text}
</Link>
)
)}
</nav>
}
export default Navbar;
// src/app/test/page.tsx
import * as crypto from "crypto";
const getData = async () => {
const res = await fetch('http://localhost:3000/random-data', {cache: 'no-store'});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
export default async function Page() {
return <p>{crypto.createHash('sha256').update(JSON.stringify(await getData())).digest('hex')}</p>
};
I've recently asked about this same topic on their feedback discussion on github: https://github.com/vercel/next.js/discussions/41745?sort=new#discussioncomment-4620262
The cause of the problem is Link is only doing client side navigation and seems to serve a cached state of the previously visited component. You'll notice that the client never calls back to the server and thus the server component never runs the second time.
I've been searching for days, but haven't found a way to force Link to reload or force refresh the component to re-render.
My conclusion is that if you have dynamic data that needs to refreshed periodically, it's best to render it in a client component and not use a server component for now.
Also, if you'd like to use Suspense, you'll need to use a library like SWR or React Query for any client side data fetching.

How can I avoid prop drilling with a headless CMS? The context API I think hurts SEO

I want to use a headless CMO in my NextJs app (e.g. Sanity.io).
The content is especially important for SEO.
If I see it correctly, I can only receive the data on page-level via getStaticProps server-side to pre-render it that way (important for SEO).
If I now want to send the data from the page component to a deeply nested child, it's awkward via prop drilling.
My first thought was to use React's Context API (see code).
However, I suspect that during the build the state of the Context API does not take over the values (The SEO text for example).
So the pre-rendered page does not have the SEO content of the headless CMO.
Is there a way to send the values of the headless CMO to deeply nested children via getStaticProps without prop drilling? Or is the context API ok for this in terms of SEO / pre-render?
//pages/index.js
export default function Home({textFromGetStaticProps}) {
const value = useAppContext();
let {seotext, setSeotext} = value.content;
console.log("The State is currently: " + seotext);
console.log("The value of getStaticProps is currently: " + textFromGetStaticProps);
//Can not set this in useEffect since only runs on ClientSide!
setSeotext(() =>{
console.log("---> setCount läuft");
return textFromGetStaticProps;
})
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>
The SEO Text is {seotext}
</h1>
</main>
</div>
)
}
//Fetch headless CMO Date via getStaticProps
export async function getStaticProps(context) {
console.log("I am running Static Props");
//API Fetch of headless CMO
return {
props: {textFromGetStaticProps: "SEO Text aus StaticProps"}, // will be passed to the page component as props
}
}
//appContext.js
const AppContext = createContext();
export function AppWrapper({ children }) {
const [seotext, setSeotext] = useState("SEO Text");
const test = {seotext, setSeotext}
console.log("I am Running AppContext: " + test);
return (
<AppContext.Provider value={{
content: test,
}}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
```
Future versions of Next.js will make this much easier but in the meantime you could try using SWR or React Query to fetch data for pre-rendering and then query inside nested components.
There are a few ways to achieve this. The first that comes to mind would be to use something like the React Context API. However, you could also use something like Redux.
Here is an example using the Context API:
import React, { createContext, useContext, useState } from 'react';
const AppContext = createContext();
export function AppWrapper({ children }) {
const [seotext, setSeotext] = useState("SEO Text");
const test = {seotext, setSeotext}
console.log("I am Running AppContext: " + test);
return (
<AppContext.Provider value={{
content: test,
}}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}

Storyblok React Bridge Restarting Preview On Data Change

I want to use the new Storyblok React bridge with Next.js. In my case, there is a component directly in the _app.js (which should not change between route changes), so it looks like this:
function MyApp({ Component, pageProps: { pageData, globalData } }) {
const story = useStoryblokState(globalData.story)
console.log('rendering app')
return (
<>
<Collage story={story} />
{/* <Component {...pageData} /> */}
</>
);
}
The corresponding getStaticProps looks like this:
export async function getStaticProps() {
let sbParams = {
version: "draft", // or 'published'
};
const storyblokApi = getStoryblokApi();
let { data: pageData } = await storyblokApi.get(`cdn/stories/home`, sbParams);
let { data: globalData } = await storyblokApi.get(
`cdn/stories/collage`,
sbParams
);
return {
props: {
pageData,
globalData,
},
revalidate: 3600,
};
}
Here everything works as intended (i.e. changes to data will only rerender the collage component but not touch anything else), but as soon as I uncomment the main component, in the Storyblok preview (with localhost:3000), changing any data will restart the iFrame, just like it would be the case if no React bridge is used. I can even see that the data changes are applied just before it restarts. Any idea how to fix this?
useStoryblokState() calls useStoryblokBridge() from the #storyblok/js package. Despite its name, useStoryblokBridge() is not a react hook, but an ordinary function with side effects. It subscribes to events from the Storyblok bridge here. If the story that was passed as argument isn't the same story that is sent to the event listener via the Storyblok bridge, the event listener will reload the entire page.
https://github.com/storyblok/storyblok-js/blob/main/lib/index.ts#L40
This means that if you call useStoryblokBridge() with two different stories, the window will reload.

How to dynamically create subpages/subroutes in NextJS?

I want to create subpages dynamically like
example.com/test/index1
example.com/test/index2
example.com/test/index3
example.com/test/index4
.......
or something like this
example.com/test/[index1]
example.com/test/[index2]
example.com/test/[index3]
The subpages should be created based on the number of indexes. in the base/parent page
I am totally unable to figure out a way to handle something like this
Help would be much appreciated
Nextjs has file system based routing. To create a dynamic route for the app you just need to create a js/ts file with a name similar to [slug].js(where slug will be the route param for the dynamic route) in the pages directory. In that file, you can write all the logic for data-fetching and export a React component as default export which will be used to render the page.
For your use case, the directory structure and some pseudo-code for the page might look something like this
// directory structure
- pages/
- test/
- [slug].js
In [slug].js The example below uses getServerSideProps as data-fetching method which will be used to fetch the data required for the page on request for that page from a client.
// data-fetching methods
export const getServerSideProps = async (ctx) => {
// you have access to the route param slug in the ctx object
const slug = ctx.params.slug
// fetch the data required for the page by a database query or from a remote API
// return the fetched data as props
return {
props: /* fetched-data */
}
}
// the page component
const SomeDynamicPage = (props) => {
// props will contain the data that was returned from the data-fetching method-
// getServerSideProps
return (
<>
<h1>Some page</h1>
<div>
/* some content based on the received props*/
</div>
</>
)
}
export default SomeDynamicPage;
There are additional data-fetching methods (getStaticProps, getStaticPaths, getInitialProps) which may be useful depending on different use cases. Read more about data-fetching and dynamic routes in nextjs docs.

Launch-time initialization in Next.js static/exported site

I'm trying to use Next to power an Electron app. electron-next uses Next's static site mode for its production build, which calls getInitialProps at build-time, rather than launch-time.
start.js (initially rendered page)
import Link from 'next/link'
export default function Start({date}) {
return (
<div>
<div>Date is {date}</div> {/* <- will always be the build time */}
<Link href="/about">
<a>Take me to the About page</a>
</Link>
</div>
)
}
Start.getInitialProps = () => {
return {
date: "" + new Date()
}
}
Interestingly, using Link to navigate elsewhere does, in fact, result in a dynamic getInitialProps call.
about.js
import Link from 'next/link'
export default function About({date}) {
return (
<div>
<div>Date is {date}</div> {/* <- will be the time the link was clicked */}
<div>Important info about this app</div>
</div>
)
}
About.getInitialProps = () => {
return {
date: "" + new Date()
}
}
Is there a non-hacky way to get dynamic behavior for the initial route? I imagine this would have plenty of use cases in static sites, too.
I ended up not using getInitialProps at all. Instead, I'm using a React hook. It works basically like this:
async function useModel() {
const modelRef = useRef(null)
// This hook will render at build-time by Next.js's static site, in which
// case the conditional loading of the model will never happen.
//
// At startup-time, it will be re-renderered on the Electron renderer thread,
// at which time, we'll actually want to load data.
if (process.browser && !modelRef.current) {
const m = new Model()
await m.init() // <- Assumed to have some async fetching logic
modelRef.current = m
}
return modelRef.current
}
Then, the top-level component can easily use the presence of the model to determine what to do next:
function Start() {
const model = useModel()
if (!model) {
return <div>Loading...</div>
} else {
return <MyProperUI model={model} />
}
}
Or, you could easily rig it up to show an unpopulated default UI, or whatever.
So basically, use getInitialProps for code you want to run exactly once, server-side/build-time or client-side. Otherwise, use other means of initialization. As seen here, hooks allow for this with pretty minimal boilerplate.

Resources