For background, see the parent issue #3196329: [META] Use cases covered by the decoupled menus initiative.

When building the structure of the site, we want to be able to load the main menu independenly of the rest of the page to build the "app shell" the content will be rendered in.

This is a different use case than what #3186628: (use case #2) Decide on an API response format for menu data is adressing, and json:api is probably not the right solution for it, we could look at the rest module to provide this data.

CommentFileSizeAuthor
#4 menuform.png90.67 KBgabesullice

Comments

nod_ created an issue. See original summary.

nod_’s picture

Issue summary: View changes
justafish’s picture

@_nod I was just reading the app-shell article you linked to in #3186628 and I'm not clear what is required here that would differ from e.g. a fully client-side React application, as both would need the same data if the menu is to be rendered on the fly. The article linked mentions -

An application shell architecture makes the most sense for apps and sites with relatively unchanging navigation but changing content.

which I would expect to be rendered the usual way with Twig if not client-side

gabesullice’s picture

Issue tags: +Decoupled menus initiative
StatusFileSize
new90.67 KB

Background

I think a primary challenge here is to avoid making a lot of unnecessary work for ourselves by trying to bend Drupal so much that it breaks. What do I mean?

I think we should avoid the existing APIs for menu_link_content entities. I think our endpoint should be a read-only endpoint to fetch "menu elements" (since that is similar to what Drupal calls them (see MenuLinkTreeElement)). Menu elements are the individual items in a menu that are supposed to be rendered as links. There are many ways to create these in Drupal:

  1. You can provide them via YAML files (e.g. mymodule.links.menu.yml).
  2. You can provide them as PHP plugins (e.g. /core/modules/user/src/Plugin/Menu/LoginLogoutMenuLink.php).
  3. You can provide them via content (e.g. enable the menu_link_content module and create entities with the same name).

The existing JSON:API endpoints for menu_link_content entities are only related to #3 and those endpoints were never really intended to be used for rendering a menu. Rather, they're a consequence of JSON:API providing endpoints for all entities of any type. Those endpoints might work if you're trying to recreate an admin UI for customizing a menu, but they're going to unwieldy if all you want is to render menu items.

If we try to retroactively make those JSON:API endpoints work, we're going to have lots of extra work for ourselves. E.g. the "view" operation for the menu link content entity type requires the administer menu permission, because it's about viewing the entity itself, not the menu element that it provides configuration for. That means creating new permissions and/or changing access control handlers. It will also mean trying to weave menu elements that come YAML and PHP into those responses, which will cause BC concerns and force us to write extra logic to block requests that would have been fine before (e.g. reject PATCH requests for plugins, but not menu_link_content entities) and that means we'll have to really mess with the JSON:API module's guts. Let's not do that 😅

Proposal

Let's create an entirely new endpoint at /jsonapi/menu-tree/{menu_machine_name}.

Why?

  1. A new endpoint avoids any BC concerns.
  2. By choosing the JSON:API format, we avoid a bikeshedding the structure of the response JSON and status codes. It also means developers can reuse the JSON:API clients they may already have.
  3. By going with menu_machine_name instead of UUID, it will be easier to consume. There is no easy way to find a menu's UUID for non-Drupalists (you have to export config and find the right YAML file). The machine name is right there in the UI (see attached screenshot)
  4. By choosing to use /jsonapi we can hook into the existing JSON:API configuration for base URLs (so you someone could configure it to be /api if they wanted to)

What would the response body look like?

Well, because it would use the JSON:API format, we would already have a headstart on this one. An example response document is below.

Things to note:

  1. The primary data is a single object, not an array of objects. This is so that we have a place to put the menu title (e.g. "Main navigation")
  2. The resource object type fields are menuTree and menuTreeElement. This is in contrast to typical JSON:API types, e.g. menu--menu, to highlight that these are not typical JSON:API entities (not perfect, but it's a start).
  3. The description attribute is from the "Administrative summary" field on the attached screenshot.
  4. There is no weight attribute. That's because JSON arrays preserve order and the JSON:API spec allows us to give meaning to relationship order. That means we can encode the weight right into the JSON structure itself.
  5. There is no enabled field. That's because I think we should omit these (and their children) automatically. That's business logic that Drupal should handle for the developer.
  6. Theres is no expanded field. That's because this endpoint is for the global and static use case described in this issue.
  7. The menu elements are under the children relationship field on both the menuTree and menuTreeElement. By treating them as relationships, the JSON:API spec allows us to include them by default. However, in the future, we could add support for the include query parameter. That would be neat because a developer could use ?include= to only get the navigation info, ?include=children to only get the first level of depth, ?include=children.children.children to get three levels of depth, or omit the parameter altogether to get all levels of depth.
  8. The target is a "real" JSON:API link (i.e. see the location link). This is because menu link targets often use URIs like internal:/node/1, which are not useful to a front end developer. By using a link, we signal that it's an interpreted field and that it can be used "as-is" since it's pointing to a real URL.

Known drawbacks

  1. It's big and verbose.
  2. It's not automatically structured in a hierarchical format.
  3. location is a made-up string. i.e. it's not an registered link relation.

I think we can justify these drawbacks though. (1) The size of is mitigated because this is intended to serve an application in a single request. You just get it once and that's it. (2) This can be mitigated by using a JSON:API client. The point is to avoid bikeshedding the structure too much. (3) This is kind of unavoidable since providing menu elements as "data" instead of "hypermedia" goes against the way links are really supposed to be used on the web (our implementation for the stateful, page to page use case can be the "pure" hypermedia design)

Example response body

{
  "data": {
    "type": "menuTree",
    "id": "main",
    "attributes": {
      "title": "Main navigation",
      "description": "Site section links"
    },
    "relationships": {
      "children": {
        "data": [{
          "type": "menuTreeElement",
          "id": "link--standard.front_page"
        }]
      }
    }
  },
  "include": [{
    "type": "menuTreeElement",
    "id": "link--standard.front_page",
    "attributes": {
      "title": "Home"
    },
    "relationships": {
      "children": {
        "data": [{
          "type": "menuTreeElement",
          "id": "item--1"
        }]
      }
    },
    "links": {
      "location": {
        "href": "https://www.example.com/"
      }
    }
  }, {
    "type": "menuTreeElement",
    "id": "item--1",
    "attributes": {
      "title": "About",
    },
    "links": {
      "location": {
        "href": "https://www.example.com/about"
      }
    }
  }]
}
nod_’s picture

That looks really really good :) And the arguments make a lot of sense. I like that we use the menu machine name because that's way more friendly than the uuid you're right. Agreed about enabled and expanded attributes.

I'm not sure about the description though, it's supposed to be privileged information, you should have the administer menu permission to view it and we'd be leaking admin data adding it to the response.

The hierarchy thing a small lib could do the job on our end to make it easier to consume.

One question I have is for contrib modules that add images or attributes to menu links where would this data be in the tree? under a generic 'extra' key or they get to choose where they add informations?

gabesullice’s picture

I'm not sure about the description though, it's supposed to be privileged information, you should have the administer menu permission to view it and we'd be leaking admin data adding it to the response.

Ah, yes. Good point. Let's not include it.

The hierarchy thing a small lib could do the job on our end to make it easier to consume.

Yep! I think it would be cool if the library could output the same object whether you get it from this endpoint or from the data we add to a response for #3186628: (use case #2) Decide on an API response format for menu data. That would mean you could switch between methods without changing your menu component.

One question I have is for contrib modules that add images or attributes to menu links where would this data be in the tree? under a generic 'extra' key or they get to choose where they add informations?

I think we could put them under attributes and relationships. Do you see a problem with it that I'm missing?

nod_’s picture

nope, no problems, we simply need to give guidelines for contrib to put what where, they might not be familiar with json:api structure :)

a lib that would remove the differences between the 2 apis sounds great, yay for convergence.

nod_’s picture

I'd like to timebox this discussion between now and monday, feb 22nd.

To keep things clear, if there are disagreement on the use cases, please discuss them first on the following issue: #3196329: [META] Use cases covered by the decoupled menus initiative. Implementation details should be discussed only if there is an agreement on the exact use case we're talking about, otherwise it will never end :)

I know it's clear but just in case: we need to agree on what we're trying to solve (the use case) before we talk about how we're going to solve the thing.

I'm happy to make time for a video/audio/chat/email/drawings meeting with hours suitable for any timezone necessary over the coming weeks if that can help facilitate discussion.

deciphered’s picture

I'm not sure about the description though, it's supposed to be privileged information, you should have the administer menu permission to view it and we'd be leaking admin data adding it to the response.

A decoupled application providing an administrative experience will need description.

Otherwise, this looks like a viable approach.

nod_’s picture

Title: Provide an API response for the "app shell" use case » Provide an API response for "stateless menu" use case
nod_’s picture

Title: Provide an API response for "stateless menu" use case » (use case #1) Provide an API response for "stateless menu" use case
nod_’s picture

gabesullice’s picture

X-post from #3196342-2: Create an endpoint for getting all menu elements for a single menu :

Rather than invent our own format, I've been prototyping the endpoint using Linkset draft as the format for a use case #1. It has not been adopted as an RFC, but it has been picked up by the IETF's HTTP API working group and I expect it to be adopted without too many changes. Since this is technically a contrib module, I think working from a draft is fine for the time being. This is actually an nice opportunity to contribute outside the Drupal ecosystem and the internet more broadly.

If we use this draft, we could make a JavaScript library for it that would be useful outside of the Drupal ecosystem, just like the once library 🙂

You can see exactly what the endpoint is expected to look like in the tests. This is what I have so far:

  1. Route: /system/menu/{menu}/linkset
    • {menu} is a parameter for a menu config entity's machine name.
      • Example output
    gabesullice’s picture

    nod_’s picture

    for those watching at home, there is a patch for an implementation of this in the related issue

    brianperry’s picture

    Status: Active » Fixed

    Since this use case has been implemented in the contrib module, and also ported to core in the following issue: https://www.drupal.org/project/drupal/issues/3227824 I'm changing the status here to fixed.

    Status: Fixed » Closed (fixed)

    Automatically closed - issue fixed for 2 weeks with no activity.