Holding page for CPT including posts of that CPT (eg /events)


trying to consolidate a few discussions around this topic with a specific scenario

let’s say I want an Events page which has both intro copy (the content field) and a paged list of events.

there’s usually 2 ways to do this in Wordpress (assuming a CPT “event” and some custom routing)

a) an /events archive template that pulls in content from a separate Events page (usually with the slug /events in the WP admin, so requires some routing overrides)


b) an Events page /events that runs a custom loop to get all the CPT items

I think a) is usually easier to paginate since WP sees it is an archive by default.


I am currently creating the CPT with CPT UI plugin. I’ve got the REST option set to true and the archive slug set to event-archive (although actually it would probably just want to be events)

In frontity, I want to be able to

  1. display this Events page with content + list when I visit /events
  2. have access to the list of events posts for rendering on other pages if necessary

the problems here are

  1. if I set the archive to "/events" in settings for my post type then I can’t fetch data from the Events page which also has the same post slug "/events"

  2. if I set the archive setting to /event-archive (matching what’s in the WP CPT admin), then it creates a browser route at /event-archive which I don’t want but at least allows me to call eg this in my events.js file

  const event_data = state.source.get(state.router.link) // ('/event-archive')
  const events = event_data.items

  const post_data = state.source.get("/events") // my Events page
  const post = state.source[post_data.type][post_data.id]  

however as noted, I actually want the user-facing URL for this page to be /events which ideally I’d set as the archive slug

is the only option for my /events URL to create a custom handler to compose the Page & Archive data together? is there another way to access the Events Page data other than source.get('/events') ?

handler example: (adapted from three-bunnies/handlers.js at master · annabranco/three-bunnies · GitHub - thanks @annya.branco !)

// get all events
// note it actually doesn't use our archive slug from WP (set as "event-archive" in CPT UI)
const eventsList = await libraries.source.api.get({endpoint: "event"}) 

// get "events" page itself. 
// this is where we will get page content from to show above our events list
const eventsPage = await libraries.source.api.get({endpoint: "pages", params: { include: 11 }});

thanks for any clarification

A couple of additional notes

  • I want to be able to navigate to eg /events/page/2 so I imagine an archive page that I pull a separate post’s content field into would be the simplest option

  • if /events shows an archive page as per above, presumably the simplest solution to avoiding the /events slug clash is just to call my Events Page in the wp admin something like /events-page and then just prevent the user going directly to it in the browser, but can at least be queried with a get?

Frontity isn’t doing anything different than what WordPress is returning. So if it works, without ugly hacks, in WP it will work in Frontity.

However there’s a big difference between the archive slug, a page slug and a post type!

In WordPress you can set both the post type and the slug of the CPT to events and disable the archive for that CPT.
That way you can create your own “archive” page with the same slug, as long as it doesn’t have any child pages.

Hi @Johan thanks.

post type would be “event”, archive would be “events”. The problem is I also have a Page “events” (ID = 11) and Frontity won’t let me query it using get(‘/events’) if there’s also an archive with that url as well

I don’t see another away to grab the post data other than using the api endpoint in a custom handler (you can pass ID to a post endpoint), unless there’s a quick workaround that avoids the need for a handler?


But you can disable the archive page for a CPT, while the slug for the items still exists.
This works both in a scripted CPT and in any (good) CPT management plugin.

That way you can still get the events page, and have access to the CPT items, without worrying about the duplicate URI.

@Johan ah right thanks, but how does Frontity access the items?
(other than creating a custom handler and populating)

get(‘/events’) is still going to get my WP page not the archive though?

if I then create a post type archive endpoint in my Frontity settings (eg /event-list), that also then creates a public route for the browser at example.com/event-list which I don’t want. It would be useful to be able to specify these endpoints as optionally only for data fetches


Ps however this maybe feels the wrong way round. I probably want /events to be my archive endpoint and then pull in the Page content somehow but again since that has the /events slug I’m stuck. In that case I guess just rename my page in the admin to /event-page and then add a handler to block that url in the browser so it’s only used as a fetch route.

Essentially the same issue, either way round it is implemented… I’ve got a browser url that I only want as a data fetch route

@Johan I managed to make the following handler and it also paginates fine, but I lose my Yoast title for the archive (if I set it as a page type rather than an archive - as noted in the code - I get the “Page” title from WP though)

const eventsFunc = async ({ route, params, state, libraries }) => {
  // https://community.frontity.org/t/how-to-list-posts-of-categories-and-its-sub-categories/572/
  // get the page number for pagination
  const { page } = libraries.source.parse(route);

  // 1. get all events (paginated 2 per page)
  const eventsList = await libraries.source.api.get({
    endpoint: "event", // note it doesn't use our "archive" slug 
    params: { 
      per_page: 2,
      page: page, 
      paged: page     

  // 1. get "Events" page itself (/events). 
  // this is where we will get page content from 
  // to show text content above our list of events
  const eventsPage = await libraries.source.api.get({
    endpoint: "pages",
    params: { include: 11 }

  // 2. add everything to the state.
  const events = await libraries.source.populate({
    response: eventsList,

  const [_page] = await libraries.source.populate({
    response: eventsPage,
  // 3. add info to data
  Object.assign(state.source.data[route], {
    // this method will show the title from the Wordpress "Page"
    isEventsPage: true,
    isPostType: true,
    type: "page",
    id: _page.id,

    // or...

    // this method is missing the archive title from Yoast
    // appears to be no condition for it in <Title> component?
    isEventArchive: true,
    isPostTypeArchive: true,
    isArchive: true,
    type: "post",

    events: events.map(event => ({
      type: event.type,
      id: event.id,
      link: event.link

export const events = {
  priority: 10,
  pattern: "/events",
  func: eventsFunc

as mentioned I lose the title.

Now if I do the following…

  • set my archive slug as “events” in CPT UI
  • set my post type with archive: "events" in frontity settings,
  • rename my Events page slug (id=11) to eg page-content so there’s no clash
  • remove my “events” handler

then my Yoast title turns up properly “Events Archive - My Site”

(I can’t paginate though because it doesn’t let me set an amount of items per page)

however as soon as I add the handler (because I need to inject some extra page content from page id=11 into the output) then the title breaks.

(see also: How to extend default data with handler? , I can’t seemingly just set it as an archive in the handler as it’s losing connection to the original data)


But you can disable the archive page for a CPT, while the slug for the items still exists.

one of the main issues is I want it to use the standard archive URL so it plays nice with yoast’s archive titles etc.

that means I need to have my archive as /events
it also means I can’t have my dummy content page as /events as it will clash as discussed

however if i rename my content page as eg events-content it’ll then also be accessible in the URL as http://mysite.com/events-content which I don’t want direct access to

I cant 404 that URL as it will also stop the fetch('/events-content') working (see…
Allow route for fetch only, not as public-facing browser url)

if i try create a custom handler to deal with this, it breaks the archive (see How to extend default data with handler?) and the connection to yoast etc


PS I made a messy solution but it seems all kinds of wrong!!

beforeSSR: async ({ state, actions, libraries }) => {
  if(state.router.link.startsWith('/events/')) {      

    // prefetch  extra content from dummy holder page
    const response = await libraries.source.api.get({
      endpoint: '/wp/v2/pages?slug=events-content'

    // add the content to our data
    const res = await response.json();
      { holder_content: res[0].content }

now frontity no longer relies on a fetch('/events-content') so I can now 404 that route in the browser to stop the user accessing it directly

also my isEventArchive params etc remain intact, and there’s now an extra bit of data that I need: holder_content which is the content field from my /events-content page in WP.

normally presumably I’d not want to put the holder page content directly into the data and instead have an indirect reference something like eg

holder: {type: "page", id: 2, link: "/events-content/"},
items: [ 
  0: {type; "event", id: 13, link: "/event/the-first-event/" },
  1: {type; "event", id: 14, link: "/event/the-second-event/" },

but as discussed above, I’ve no way to disable the browser url for /events-content without disabling the fetch route, hence my need to rely on the api get and putting the holder page content directly into the data, so that method would leaves me with the same problem anyway.

@Johan I ended up solving like this, without handlers for simplicity (and also due to this handler issue How to extend default data with handler?)

it allows me to easily use the proper pagination eg /events/page/2 etc and connects to Yoast properly

beforeSSR: async ({ state, actions, libraries }) => {
  if(state.router.link === '/events-page/') {

    // manually 404 this route, ie when visiting mysite.com/events-page
    Object.assign(state.source.data[state.router.link], {
      isError: true,
      is404: true,
      isPostType: false,
      errorStatus: 404
  // fetch route still works
  if(state.router.link.startsWith('/events/')) {
    // grab the content from our dummy page
    await actions.source.fetch("/events-page/")
  • index.js switch (/events)
    <Events when={data.isEventArchive} />

  • <Events> component for isEventArchive

const Events = ({ state, actions, libraries }) => {

  // get our event archive items
  const data = state.source.get(state.router.link)
  const events = data.items 

  // get our dummy holder page
  const events_page_data = state.source.get("/events-page")
  let events_page
  // check if data is loaded, 
  // since it will initially be undefined when browsing client-side
  if(events_page_data.isReady) {
    events_page = state.source[events_page_data.type][events_page_data.id];  

  // client-side fetch of content
  useEffect(() => {
  // check archive content and holder page content are ready
  return data.isReady && events_page_data.isReady ? (

  {/* render holder content */}
  <div dangerouslySetInnerHTML={{__html: events_page.content?.rendered }}>

  {/* render archive list */}
  { events?.map(event_item => {.... etc
1 Like