Handlers for non-URL data

I’m opening this topic to discuss how to work in wp-source with handlers with non-URL data like comments, menus, widgets, attachments, etc. I was thinking of working on the comments handler but we should take a decision before.

As far as I know, our initial idea was to keep the same APIs than the URL data, but in these cases, it doesn’t start with “/”. Is it right?

In the comments handler, and I guess it applies to other ones too, we should decide what is the best approach for fetching and populating the state.

  • Should we fetch comments by post id? Something like:
    // Fetch comments for post 123:
    actions.source.fetch("comments/123")
  • How should we populate the state? We could include all the comments under state.source.comments[commentId] just like posts, categories, tags… or we could include the postId directly under comments -> state.source.comments[postId][commentId].

What do you think guys? I’m aware you’ve thought about this more than me, so I guess you’ll know more aspects I didn’t consider :slightly_smiling_face:

2 Likes

Yes. One idea is to simply not to use a slash, like you comment. Another idea is to add a specific character that makes it clearer that it’s not a URL, for example:

actions.source.fetch("!comments/123")

It’d be a standard, of course. Nothing constrained by the code.

Yes, I think fetching by post id is the best option.

This has to be state.source.comments[commentId] because that’s the way everything is stored in Frontity.

In order to know which comments belong to a post, uses will access state.source.data["comments/123"] and look at the items array with the ids.

We have to make a decision if there are more than 100 posts because the REST API is limited to 100. So either something like state.source.data["comments/123/page/2"] or the handler of comments/123 does multiple fetches until it has all the comments.


Regarding attachments, I think we could use a similar syntax by id:

actions.source.fetch("attachment/123")

@SantosGuillamot could you please investigate how the official REST API plugin of menus is going to work? https://github.com/WP-API/menus-endpoints

Sure, I took a quick look some time ago but I’ll go deeper and share here the findings.

I’ve installed the plugin in test.frontity.io. It looks like this is still in the very beginning.

I’ve also installed this one: https://wordpress.org/plugins/wp-rest-api-v2-menus/.

At a first glance, none of them offer a clear structure of the children elements of each element. Maybe we can generate the proper hierarchy in Frontity itself, instead of relying on what these plugins are exposing.

Also, in the “official” one, there’s no way to fetch all the items using the menu slug, only the id. You either use the id or do an initial fetch of the menu to discover the id.

A third option would be to release our own PHP plugin (like what we are doing for Yoast Meta).

Thought I’d chime in with my 2c about this from here.

While working on my own project I’ve noticed a few limitations in the official plugin:

  • Unable to retrieve menus via slug
  • Returns a flat array of all menu items
  • Menu items return the absolute URL instead but not the slug

I’ve recently started working with Frontity to build a client site and had the requirement to build a super simple menu with some dropdown items. As we’re using Bootstrap React we needed the menu items formed in a way to replicate the dropdown structure:

<Dropdown.Toggle variant="success" id="dropdown-basic">
    Dropdown Button
</Dropdown.Toggle>

<Dropdown.Menu>
    <Dropdown.Item href="#/action-1">Action</Dropdown.Item>
    <Dropdown.Item href="#/action-2">Another action</Dropdown.Item>
    <Dropdown.Item href="#/action-3">Something else</Dropdown.Item>
</Dropdown.Menu>

Because the official plugin simply returned a single level array of every menu item, and the absolute URL we had to restructure the array before trying to build our Bootstrap menu. We had to move the child menu items into a children property in order to easily figure out where the menu item should belong and strip out the WP install path from the URL property in order to retrieve just the slug. We’ve settled for the below code snippet at the moment, it may go through some further refactoring but works currently:

var data = {...} // JSON response from WP
var config = {...} // Some properties (domain as an example to replace in the menu)
var menu = [];
data.forEach( item => {
  if( item.menu_item_parent === 0 ) {
    let menuObj = {
      "title": item.title.rendered,
      "link": item.url.replace(config.base, ''), // drop the domain / wp install folder so we're left with just the slug
      "id": item.id,
      "parent": item.menu_item_parent,
      "classes": item.classes
    };

    // Try and find children
    let children = data.filter( (menuItem) => {
      return menuItem.menu_item_parent === item.id;
    }).map( (child) => {
      return {
        "title": child.title.rendered,
        "link": child.url.replace(config.base, ''), // drop the domain / wp install folder so we're left with just the slug
        "id": child.id,
        "parent": child.menu_item_parent,
        "classes": child.classes
      };
    });
    menuObj["children"] = children;
    menu.push(menuObj);
  }
});

While there could be some code improvements I’m sure, this seems like alot of code for a simple menu. It would be ideal if each menu item in the response from WP had:

  • An array of children
  • The slug so we don’t need to manually strip the URL property

With these 2 simple changes it would cut out most (if not all) the code required to render the menu in our application.

1 Like

Yeah, I see. Thanks for sharing.

Regarding how to fetch a menu:

  • The name is the more use friendly one and easy to find in the WP Dashboard. But if the name of the menu is changed, you have to change your frontity.settings.js file.
  • The slug is more user friendly, but it’s also hard to obtain from the WP Dashboard. Actually, I don’t know how. Also, if you change the name, the slug changes as well. It is more URL friendly than the name because it doesn’t have special character, spaces and so on.
  • The id doesn’t ever change, which is great. But it doesn’t give you information about which menu is when you copy and paste it in frontity.settings.js. You can get it in the WP Dashboard looking at the URL, but only after you click “Select”, which is probably not intuitive enough.

If we want to use the official plugin and respect the structure we’ve been following for the rest of the WP content, my initial thought is that it should be something like this:

state.source.data["menus/1"] = {
  isMenu: true,
  type: "menu",
  id: 1,
  items: [
    {  type: "menu-item", id: 2 },
    {  type: "menu-item", id: 3 },
    {  type: "menu-item", id: 4, children: [
      { type: "menu-item", id: 5 },
      { type: "menu-item", id: 6 },
      { type: "menu-item", id: 7, children: [
        { type: "menu-item", id: 8 },
        { type: "menu-item", id: 9 },
      ] },
    ] },
    {  type: "menu-item", id: 10 },
    {  type: "menu-item", id: 11 },
  ]
};

state.source.menu["1"] = {
  id: 1,
  description: "My menu description",
  name: "My First menu",
  slug: "my-fist-menu",
  meta: []
};

state.source["menu-item"]["2"] = {
  id: 2,
  title: {
    rendered: "Home"
  },
  status: "publish",
  link: "/", // <- remove the domain
  attr_title: "",
  description: "",
  type: "custom",
  type_label: "Custom Link",
  object: "custom",
  object_id: 2,
  parent: 0,
  menu_item_parent: 0,
  menu_order: 1,
  target: "",
  classes: [""],
  xfn: [""],
  meta: []
};

We can remove the domain from the URL as we do for the link field of posts and taxonomies.

The React component for the menu could be something like this:

const Menu = ({ state }) => {
  const data = state.source.get("menus/1");
  return (
    <ul>
      {data.items.map(item =>
        <MenuItem id={item.id} children={item.children} />
      )}
    </ul>
  );
}

const MenuItem = ({ state, id, children }) => {
  const item = state.source["menu-item"][id];
  return (
    <li>
      <Link href={item.link}>{item.title.rendered}</Link>
      {children && (
        <ul>
          {children.map(child => {
            <MenuItem id={child.id} children={child.children} />
          }
        </ul>
      )}
    </li>
  );
);

Feedback is welcomed!

By the way, we’ve included the URL helper in Frontity: https://docs.frontity.org/api-reference-1/frontity#url

You have to import it from "frontity" to make sure it works for both the server and the client:

import { URL } from "frontity";

const url = new URL(item.url);

url.pathname // <-- the relative path

More info about URL in the mozilla.org docs:

Ah I see, must have missed that looks like I have some refactoring to do!

Perhaps I’ve not thought about it enough, have missed something or perhaps it even has a valid use-case, but have you considered retrieving menu via location? Only 1 menu can be assigned to a location, they’re fairly static and dev’s should (I hope) know what menu locations have been set up in their theme.

The response from WP would remain the same as your suggestion and if there is a valid use case for retrieving menus not assigned to a location an additional handler can be used to retrieve the menu via ID.

I assume you’ve already thought about this, but would love to hear your thoughts or if I’ve missed any glaringly obvious issues with doing it like this.

@SantosGuillamot has started a PR for the first non-URL handler: comments!

Hi, this is my first comment. I’m in investigation frontity for my project, and from my perspective this is great thing. Currently, I’m trying to implement menu, but I can’t understand location for code above.

1 Like

Hey @nihad.obralic, welcome to the community :slight_smile:

This is still a work in progress. I recommend you to stick to a simpler menu, probably stored in the frontity.settings.js file (like the one in mars-theme) for now.

When we finish this work we’ll announce support for WP menus, edited in the dashboard.

Thank you.

I’m using wp-rest-api-v2-menus/ plugin for now. I’m able to get all necessary data, but there are also plenty of redundant data. Imho it would be great to us “Wordpress Frontity Menu Rest” plugin. Which could return tailor-made data for Frontity needs.

I’ve also played a bit with WP-API/menus-endpoints mostly with widget part. And so far widget part is totally useless.

I was trying to get widgets for particular sidebar:
http://example.com/wp-json/wp/v2/widgets?sidebar=default
and this is what I’ve got:

[
  {
    "id": "custom_html-2",
    "type": "custom_html"
  },
  {
    "id": "media_gallery-2",
    "type": "media_gallery"
  },
  {
    "id": "links-2",
    "type": "links"
  },
  {
    "id": "recent-posts-2",
    "type": "recent-posts",
    "title": "recent posts",
    "number": 5,
    "show_date": false
  }
]

I expected totally different data. I thougt this endpoint will return let’s say html for every single widget.
I finished with custom rest endpoint. I’m not a PHP developer, but friend of mine prepared this code for me:

<?php
function get_sidebar($data) {
	$id = urldecode($data['id']);
	ob_start();
	dynamic_sidebar($id);
	$sidebar = ob_get_contents();
	ob_end_clean();
	return array('id'=>$id, 'content'=>$sidebar);
}

function get_sidebars() {
	$results = array();
	foreach ( $GLOBALS['wp_registered_sidebars'] as $sidebar ) {
		$results[] = sbc_get_sidebar(array('id'=>$sidebar['id']));
	}
	return $results;
}

add_action( 'rest_api_init', function () {
	register_rest_route( 'wp/v2', '/sidebars', array(
		'methods' => 'GET',
		'callback' => 'get_sidebars',
	));
	register_rest_route( 'wp/v2', '/sidebars/(?P<id>[%a-zA-Z0-9_-]+)', array(
		'methods' => 'GET',
		'callback' => 'get_sidebar',
		'args' => array(
			'id' => array()	
		)
	));
});
?>

I think the best solution for all those problems could be a set of wordpress plugins dedicated for Frontity:

  • Yoast Meta Frontity,
  • Menu Rest Frontity,
  • Sidebars Rest Frontity.
    and so on.
1 Like

Hi @LiamMcKoy, we are about to release the Yoast Plugin in the next days. It will be a more general plugin because it will expose all tags in <head> and will work with other SEO plugins, so we are considering to change its name to something like REST API Head Tags.

About the others, we have already in mind to create a bunch of plugins to help WordPress integrate with Frontity, so thanks for your suggestions! :blush:

1 Like

An update about the menus endpoint on WordPress. It looks like it will probably get merged in the WordPress core in the 5.4 version:

1 Like

Hi there! :wave:

We’re planning to start working on the comments package in the coming weeks, but first we have to take a decision about handlers for non-URL data. Here we have a quick summary so we can continue the discussion:

  • Our initial idea was to use the same API than the URL data, but doing minor changes like not using the initial slash or adding any character before:
actions.source.fetch("comments/123") //without slash
actions.source.fetch("!comments/123")

It would be and standard, nothing constrained.

  • We have to populate the state the same way everything is stored in Frontity. For example, the comments should be stored in state.source.comments[commentId].

Any thoughts? Anything to discuss about?

I think that it’s going to be more obvious that this is not a URL if we use a distinctive character.

We can modify actions.router.set so it throws when the link passed doesn’t start with "/".

If we do that, we can use any character we want, reserved or unreserved:

// Throws because it's not a valid URL.
actions.router.set("comments/123");
// Throws because it's not a valid URL.
actions.router.set("@comments/123");

// Doesn't throw, this could be a valid URL:
actions.router.set("/comments/123");
// Doesn't throw, this could be a valid URL:
actions.router.set("/@comments/123");

From the list of possible characters:

reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
unreserved = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"

I like actions.source.fetch("@comments/123") the most.


We also need to make a decision about this :point_up_2:

But maybe it’s better to open a feature topic for the comments handler to start discussing about this @SantosGuillamot? :slight_smile:

I like the approach of using a special character, I agree is more obvious and we can throw errors if it doesn’t start with "/" as you said.

Regarding which character, maybe we should try to use one that is not commonly used for other “code” purposes, so it isn’t confusing. For example, I’m aware that the "$" character could be used inside regex or template strings right? Or the "!" could be confused with the logical “not” operator. Not sure if these would be confusing or not, you’ll know better about this.

Btw, I think "@" is not confusing at all, so we can go with it.

Sure, I’m planning to add two new topics for both the menu and the comments. I wanted to keep this topic just for the handlers for non-URL data. I’ll reference them here once they are open.

Awesome! :clap:

By the way, should we open a feature topic for the comment handler and a feature topic for the comment package or only one for the comment package that contains a user story for the comment handler? What do you think?

I thought about that some time ago, and I think it makes sense to split it, although I’m not 100% convinced :grin: .

Right now, Frontity is handling all the WordPress default content like posts, CPT, taxonomies, authors… and populating the `state, and the themes decide how to consume this data. I think WordPress works in a similar way: you can get all the comments with a PHP function, but the theme decides how to show them.

So I feel it could be a good approach to create a handler that populates the state in an easy way to consume, and add an example of how to consume it in mars-theme.

In the future, if we see this is not enough, we can work on the feature of building a comments component.

Btw, I think is something that will happen again, for example with the menus.