are you using WPGraphQL as your endpoint? you also more than likely need to have a bearer token unless your endpoint can be publicly introspected. Ill post a few files here from a current headless wordpress build I have
First, consider using something like apollo client to provide consistent caching globally. SWR and GraphQL Query are fantastic, too.
dynamic-nav.graphql
# import DynamicNavFragment from '../Partials/dynamic-nav-fields.graphql'
query DynamicNav(
$idHead: ID!
$idTypeHead: WordpressMenuNodeIdTypeEnum!
$idFoot: ID!
$idTypeFoot: WordpressMenuNodeIdTypeEnum!
) {
Header: wordpress {
menu(id: $idHead, idType: $idTypeHead) {
menuItems(where: { parentId: 0 }) {
edges {
node {
...DynamicNavFragment
childItems {
edges {
node {
...DynamicNavFragment
childItems {
edges {
node {
...DynamicNavFragment
}
}
}
}
}
}
}
}
}
}
}
Footer: wordpress {
menu(id: $idFoot, idType: $idTypeFoot) {
menuItems(where: { parentId: 0 }) {
edges {
node {
...DynamicNavFragment
childItems {
edges {
node {
...DynamicNavFragment
}
}
}
}
}
}
}
}
}
The fragment being used is
dynamic-nav-fields.graphql
fragment DynamicNavFragment on WordpressMenuItem {
id
label
path
parentId
}
It all comes together in a global layout.tsx
component. There is a lengthy headless-nav component I'll include below but that's because it is 3 layers deep with dynamic subsubanchors
layout.tsx
import { FooterFixture } from './Footer';
import { HeadlessFooter } from './HeadlessFooter';
import { HeadlessNavbar } from './HeadlessNavbar';
import { Navbar } from './Navbar';
import { Meta } from './Meta';
import cn from 'classnames';
import {
DynamicNavQuery,
useDynamicNavQuery,
WordpressMenuNodeIdTypeEnum
} from '@/graphql/generated/graphql';
import {
ApolloError,
Button,
Fallback,
Modal,
LandingHero
} from '../UI';
import { useGlobal } from '../Context';
import { useAcceptCookies } from '@/lib/use-accept-cookies';
import Head from 'next/head';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
// import { LoginProps } from '../Auth/wp-login';
const dynamicProps = {
loading: () => <Fallback />
};
const LoginView = dynamic(
() => import('../Auth/wp-login'),
dynamicProps
);
const RegisterView = dynamic(
() => import('../Auth/wp-register'),
dynamicProps
);
const Featurebar = dynamic(
() => import('./Featurebar'),
dynamicProps
);
export interface LayoutProps {
Header: DynamicNavQuery['Header'];
Footer: DynamicNavQuery['Footer'];
className?: string;
title?: string;
hero?: React.ReactNode;
children?: React.ReactNode;
}
function AppLayout({
Header,
Footer,
className,
title,
children,
hero
}: LayoutProps) {
const { acceptedCookies, onAcceptCookies } = useAcceptCookies();
const { displayModal, modalView, closeModal } = useGlobal();
const { loading, error, data } = useDynamicNavQuery({
variables: {
idHead: 'Header',
idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
idFoot: 'Footer'
},
notifyOnNetworkStatusChange: true
});
const router = useRouter();
Header =
data != null && data.Header != null ? data.Header : undefined;
Footer =
data != null && data.Footer != null ? data.Footer : undefined;
return (
<>
<Head>
<title>{title ?? '✂ The Fade Room Inc. ✂'}</title>
</Head>
<Meta />
{error ? (
<>
<ApolloError error={error} />
</>
) : loading && !error ? (
<Fallback />
) : (
<Navbar
Desktop={<HeadlessNavbar header={Header} />}
Mobile={
<HeadlessNavbar
header={Header}
className={
'block px-3 py-2 rounded-md text-lg sm:text-base font-sans font-semibold text-secondary-0 hover:bg-redditSearch'
}
/>
}
/>
)}
<>
{router.pathname === '/' ? <LandingHero /> : hero}
<Modal open={displayModal} onClose={closeModal}>
{modalView === 'LOGIN_VIEW' && <LoginView />}
{modalView === 'SIGNUP_VIEW' && <RegisterView />}
</Modal>
<div className={cn('bg-redditSearch', className)}>
<main className='fit'>
{children}
{error ? (
<>
<ApolloError error={error} />
</>
) : loading && !error ? (
<Fallback />
) : (
<FooterFixture
children={<HeadlessFooter footer={Footer} />}
/>
)}
</main>
<div className='font-sans z-150'>
<Featurebar
title='This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy.'
hide={
acceptedCookies ? !!acceptedCookies : !acceptedCookies
}
className='prose-lg sm:prose-xl bg-opacity-90 sm:text-center'
action={
<Button
type='submit'
variant='slim'
className='mx-auto text-secondary-0 text-center rounded-xl border-secondary-0 border-1 hover:bg-gray-700 hover:bg-opacity-80 hover:border-secondary-0 duration-500 ease-in-out transform-gpu transition-colors'
onClick={() => onAcceptCookies()}
>
Accept Cookies
</Button>
}
/>
</div>
</div>
</>
</>
);
}
export default AppLayout;
The HeadlessNavbar
component being injected with data from the useDynamicNavQuery
function is mapped out here. I have been meaning to fractionate this apart but it is a bit of a pain.
Layout/HeadlessNavbar/headless-navbar.tsx
import { useState, FC, useRef, useEffect } from 'react';
import cn from 'classnames';
import { useRouter } from 'next/router';
import Link from 'next/link';
import { Button } from '../../UI';
import { Transition, Listbox } from '@headlessui/react';
import { NavArrow } from '@/components/UI/Icons';
import { DynamicNavQuery } from '@/graphql/generated/graphql';
import css from './headless-navbar.module.css';
import { DynamicNavSubAnchors } from '@/types/dynamic-nav';
import throttle from 'lodash.throttle';
export interface HeadlessNavbarProps
extends DynamicNavSubAnchors {
header: DynamicNavQuery['Header'];
className?: string;
}
const HeadlessNavbar: FC<HeadlessNavbarProps> = ({
header,
className,
node
}) => {
const [isOpen, setIsOpen] = useState(false);
const [subOpen, setSubOpen] = useState(false);
const [hasScrolled, setHasScrolled] = useState(false);
const [selectedCategory, setSelectedCategory] = useState(node);
const { pathname } = useRouter();
const isOpenRef = useRef(isOpen);
const refAcceptor =
useRef() as React.MutableRefObject<HTMLDivElement>;
useEffect(() => {
const handleScroll = throttle(() => {
const offset = window.scrollY ?? 0;
const { scrollTop } = document.documentElement;
const scrolled = scrollTop > offset;
setHasScrolled(scrolled);
setIsOpen(isOpenRef.current);
}, 200);
document.addEventListener('scroll', handleScroll);
return () => {
document.removeEventListener('scroll', handleScroll);
};
}, [hasScrolled, isOpenRef.current]);
return (
<>
{header?.menu?.menuItems?.edges &&
header.menu.menuItems.edges.length > 0 ? (
header.menu.menuItems.edges.map((top, i) => {
return top != null &&
top.node != null &&
top.node.label != null ? (
<>
{top.node.childItems?.edges &&
top.node.childItems.edges.length !== 0 ? (
<>
<Link
href={top.node.path}
as={top.node.path}
passHref
scroll={true}
key={top.node.id}
>
<a
id='top'
className={cn(className, {
[css.activeWithChildren]: pathname === top.node.path,
[css.linkWithChildren]: pathname !== top.node.path
})}
>
<p className='inline-flex'>{top.node.label}</p>
</a>
</Link>
<button
onClick={() => setIsOpen(!isOpen)}
id='sub-menu'
aria-haspopup={true}
aria-expanded={true}
type='button'
className={cn(css.topButton, {
'-rotate-180': isOpen,
'rotate-0': !isOpen
})}
>
<NavArrow className='select-none w-5 h-5' />
</button>
</>
) : top.node.childItems?.edges &&
top.node.childItems.edges.length < 1 ? (
<Link
href={top.node.path}
as={top.node.path}
passHref
scroll={true}
key={top.node.id}
>
<a
id='top'
className={cn(className, {
[css.active]: pathname === top.node.path,
[css.link]: pathname !== top.node.path
})}
>
<p>{top.node.label}</p>
</a>
</Link>
) : (
<></>
)}
{top.node.childItems != null &&
top.node.childItems.edges != null &&
top.node.childItems.edges.length > 0 ? (
<div className='lg:relative z-150 -ml-2'>
<Transition
show={isOpen}
enter='transition ease-out duration-200 '
enterFrom='transform opacity-0 translate-y-1'
enterTo='transform opacity-100 translate-y-0'
leave='transition ease-in duration-150'
leaveFrom='transform opacity-100 translate-y-0'
leaveTo='transform opacity-0 translate-y-1'
>
<div className={cn(css.transitionAlpha, '')}>
<div
className={css.transitionBeta}
ref={refAcceptor}
role='menu'
aria-orientation='vertical'
aria-labelledby='sub-menu'
>
<div className={css.transitionGamma}>
{top!.node!.childItems!.edges!.map((sub, j) => {
return sub != null &&
sub.node != null &&
sub.node.label != null &&
sub.node.parentId != null ? (
<Listbox
key={sub.node.path}
value={selectedCategory}
onChange={setSelectedCategory}
>
{({ open }) => (
<>
<div className={cn(css.divOpen)}>
<p className='text-base font-medium pr-2 bg-redditNav font-sans inline-flex py-2 ml-4'>
{sub!.node!.label!}
<Listbox.Button
as='button'
aria-haspopup={true}
id='sub'
aria-expanded={true}
onClick={() => setSubOpen(!subOpen)}
className={cn(css.bottomButton, {
' bg-redditNav -rotate-180': open,
' bg-redditNav rotate-0': !open
})}
>
<NavArrow className='select-none w-5 h-5' />
</Listbox.Button>
</p>
</div>
<Transition
show={open}
enter='transition ease-out duration-100'
enterFrom='transform opacity-0 scale-95'
enterTo='transform opacity-100 scale-100'
leave='transition ease-in duration-75'
leaveFrom='transform opacity-100 scale-100'
leaveTo='transform opacity-0 scale-95'
>
<Listbox.Options
static
className='outline-none select-none focus:outline-none'
>
{sub!.node!.childItems != null &&
sub!.node!.childItems.edges != null &&
sub!.node!.childItems.edges.length > 0 ? (
sub!.node!.childItems!.edges!.map(
(subsub, k) => {
return subsub != null &&
subsub.node != null &&
subsub.node.label != null &&
subsub.node.parentId != null ? (
<>
{open && (
<Listbox.Option
key={subsub.node.id}
className={cn(
css.subsub,
'lg:text-base text-sm hover:bg-redditSearch font-medium list-none outline-none font-sans'
)}
value={subsub!.node!.label}
>
<>
<Link
href={subsub!.node!.path}
passHref
key={k++}
shallow={true}
>
<a
id='subsub'
className={cn(css.subsubanchor)}
>
{subsub!.node!.label}
</a>
</Link>
</>
</Listbox.Option>
)}
</>
) : (
<></>
);
}
)
) : (
<></>
)}
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
) : (
<></>
);
})}
</div>
</div>
</div>
</Transition>
</div>
) : (
<></>
)}
</>
) : (
<></>
);
})
) : (
<></>
)}
</>
);
};
export default HeadlessNavbar;
Here's the live site the code powers