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

Hi,

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)

OR

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
J

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?

Thanks
J

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

Thanks
J

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,
    state
  });

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

    // or...

    // TREAT AS ARCHIVE
    // 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",
    
    
    page: page, // PAGE NUMBER FOR PAGINATION

    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)

@Johan

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

thanks
J

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();
    Object.assign(state.source.data[state.router.link], 
      { 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(() => {
    actions.source.fetch("/events-page/")
  },[])
  
  // 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 }}>
  </div>

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