There should be an option to append a BreadcrumbList as defined on schema.org to each page reflecting the current breadcrumb trail.

The structured data should be added in JSON-LD like recommended from Google here.

We should only add structured data for content which is being shown on the page we add it to as defined in Google's structured data guidelines. And therefore should append it only when the breadcrumb block is displayed on the page.

TODOs:

* Add code to attach current breadcrumb as JSON-LD to every page.
* Add tests.
* Update README.
* Update module description / feature list on drupal.org.

Initial issue description:

It should be possible to use Structured data, from schema.org standard. This should also be available when the module is active and the block is not.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dscoop created an issue. See original summary.

Greg Boggs’s picture

Are you thinking you'd like the markup in JSON-LD, RDFa, Microdata, or all 3? I'd be happy to add several Twig files to the module if someone is interested in doing the work

dscoop’s picture

I was thinking about JSON-LD only. Would be really nice to have. Also looks so much better in Google search.

I was not thinking about doing this in the template itself, but adding a hook for the module, where JSON-LD was added to the header, if an option to the module was enabled. That way we could have JSON-LD breadcrumbs enabled without being able to see the actual breadcrumb on nodes.

Greg Boggs’s picture

Doing that might be a violation of Google Webmaster guidelines, so I would consider just printing the breadcrumb because they claim to validate the json-LD against your HTML to confirm that you're not feeding them cloaked content.

Having said that, if you want to do it, there's a few routes, but all of them need work to port them from D7 to D8.

https://www.drupal.org/project/jsonld
https://www.drupal.org/project/schemaorg
https://www.drupal.org/project/schema

Finally, we could add the json to easy_breadcrumb, but we'll need to make sure it plays nicely with the above modules once they are ported.

~G

dscoop’s picture

I assume that this module only print valid paths, so we shouldnt worry about violation of Google Webmaster guidelines.

I am testing this page at the moment:
https://www.cooper.dk/cases/tveast

Structured data-test can be found here:
https://search.google.com/structured-data/testing-tool/u/0/?hl=da#url=ht...

Greg Boggs’s picture

Looks like a great test so far. Feel free to send me a Pull Request on Github: https://github.com/Greg-Boggs/easy_breadcrumb, or a patch here. ;)

~G

Greg Boggs’s picture

Any updates on this feature?

sunward’s picture

subscribe

Greg Boggs’s picture

Bump.

kurtismccartney’s picture

I've got a preffered operating format. please mind the truncation of the "easy" part throughout the tested segment.

<div id="block-easy-breadcrumb-easy-breadcrumb" class="block block-easy-breadcrumb contextual-links-region first last odd"> <div vocab="http://schema.org/" typeof="BreadcrumbList">
  <div class="breadcrumb" typeof="BreadcrumbList"><span property="itemListElement" typeof="ListItem"><a href="/" class="breadcrumb_segment breadcrumb_segment-front" property="item" typeof="WebPage"><span property="name">Home</span></a></span><span class="breadcrumb-separator"> › </span><span property="itemListElement" typeof="ListItem"><a href="/events" class="breadcrumb_segment breadcrumb_segment-1" property="item" typeof="WebPage"><span property="name">Events</span></a></span></div>
</div></div>

Anyone know how to get the span wrappers on the inside and outside of the anchor, then getting these elements in the live code.

kurtismccartney’s picture

Note. Last element needed is the recode of the anchor tag.

function theme_easy_breadcrumb($variables) {

  $breadcrumb = $variables['breadcrumb'];
  $segments_quantity = $variables['segments_quantity'];
  $separator = $variables['separator'];

  $html = '';

  if ($segments_quantity > 0) {

    $html .= '<div vocab="http://schema.org/" typeof="BreadcrumbList"><div class="breadcrumb" typeof="BreadcrumbList">';

    for ($i = 0, $s = $segments_quantity - 1; $i < $segments_quantity; ++$i) {
			$it = $breadcrumb[$i];
      $content = decode_entities($it['content']);
			if (isset($it['url'])) {
        $html .= '<span property="itemListElement" typeof="ListItem">';
        $html .= l($content, $it['url'], array('attributes' => array('class' => $it['class'])));
        $html .= '</span>';
			} else {
        $class = implode(' ', $it['class']);
				$html .= '<span class="' . $class . '" property="itemListElement" typeof="ListItem"><span property="name">'	. check_plain($content) . '</span></span>';
			}
			if ($i < $s) {
				$html .= '<span class="bcs"> ' . $separator . ' </span>';
			}
    }
    
    $html .= '</div>';
  }

  return $html;
}
Greg Boggs’s picture

Perhaps it would be easier to program, and easier to read, while still being equally useful if we add the structured data as json-ld format instead of inline with the HTML?

Happy to accept the feature in any (of the formats of course).

kurtismccartney’s picture

Of course. Very amusing change, and would have preferred the JSON solution as well.

Trying to balance 2016 Greg "Doing that might be a violation of Google Webmaster guidelines" and 2017 Greg "easier to read, while still being equally useful if we add the structured data as JSON-LD format" with a bit of determination. If I could get a hard coded version working with that link wrapper then it would be much easier to consider adding a No SCHEMA, JSON-LD or RDF toggle in options.

My Drupal module experience is a bit behind on D7. I'm just a little stumped when it comes to deconstructing this:
l($content, $it['url'], array('attributes' => array('class' => $it['class'])));

So that it can output this:
<a href="/" class="breadcrumb_segment breadcrumb_segment-front" property="item" typeof="WebPage"><span property="name">Home</span></a>

kurtismccartney’s picture

Continuing to do things in a very loud and complicated fashion. Apologies for people fond of big long classes. Here is a new base for anyone poking around with RDFa:

function theme_easy_breadcrumb($variables) {

  $breadcrumb = $variables['breadcrumb'];
  $segments_quantity = $variables['segments_quantity'];
  $separator = $variables['separator'];

  $html = '';

  if ($segments_quantity > 0) {

    $html .= '<div vocab="http://schema.org/" typeof="BreadcrumbList"><div class="breadcrumb" typeof="BreadcrumbList">';

    for ($i = 0, $s = $segments_quantity - 1; $i < $segments_quantity; ++$i) {
			$it = $breadcrumb[$i];
      $content = decode_entities($it['content']);
			if (isset($it['url'])) {
        $html .= '<span property="itemListElement" typeof="ListItem">';
        $html .= l($content, $it['url'], array('attributes' => array('property' => 'item', 'typeof' => 'WebPage')));
        $html .= '<meta property="position" content="' . $i . '"></span>';
			} else {
				$html .= '<span property="itemListElement" typeof="ListItem"><span property="name">'	. check_plain($content) . '</span></span>';
			}
			if ($i < $s) {
				$html .= ' ' . $separator . ' ';
			}
    }
    
    $html .= '</div>';
  }

  return $html;
}
Greg Boggs’s picture

I was pretty new to structured data when I made the first post.

Ideally, we'd be able to provide options. But, I meant to communicate, that you're welcome to ignore my thoughts from last year if it turns out to be difficult to replicate the HTML you want directly. I think any of the 3 approaches is most likely a solid way to go.

~Greg

Greg Boggs’s picture

Issue tags: +good-first-issue
tatarbj’s picture

Issue tags: +DrupalCampBelarus2019
leymannx’s picture

Little late to the party, but here comes a first working draft.

1. I added a new tab to the module's config which is called "Structured Data". There you can enable having the current breadcrumb added as structured data in JSON-LD to the HTML head (referencing Google's tutorial on structured data for breadcrumbs). This setting works independently from having the breadcrumbs block placed on the page or not.

2. I implemented hook_page_attachments from where I called \Drupal::service('easy_breadcrumb.breadcrumb')->build($route_match); to get the current breadcrumb, build the JSON from it and have it added to the HTML head when the aforementioned setting is activated.

Certain things I'd like to have discussed:

1. Calling \Drupal::service('easy_breadcrumb.breadcrumb')->build($route_match); in the hook gives me the links without taking the Easy Breadcrumb settings into account. For example I configured Easy Breadcrumb to have no home link printed on the front page. Which works just fine for the breadcrumb block. But \Drupal::service('easy_breadcrumb.breadcrumb')->build($route_match); still gives me the link and therefore will be printed as structured data. Is this a desirable thing to have? Or how can I have \Drupal::service('easy_breadcrumb.breadcrumb')->build($route_match); respect the Easy Breadcrumb config?

2. There was this requirement to have structured data work independently from the block – which is a good idea(!) – so I decided to have it added as <script type="application/ld+json"> directly to the HTML head using hook_page_attachments or is there any better place to have that added?

3. I added '#cache' => ['contexts' => ['url.path']], for having this piece of markup considered for caching per URL. But I don't know if this is necessary or if it will be taken into account at all.

4. Should we move part of the code from inside the hook into a dedicated class or maybe even add a new method inside the EasyBreadcrumbBuilder.php?

TODOs:

- Add tests
- Update README
- Update module description here on drupal.org

leymannx’s picture

leymannx’s picture

Maybe we should rename the default "Settings" page to "Block settings" or something similar?

leymannx’s picture

Status: Active » Needs review
Greg Boggs’s picture

Great stuff!

1. You're the first person to call Easy Breadcrumb outside of the build. This sounds like a bug that needs to be fixed!

2. I'd prefer to use a service/event architecture so it's the same as the rest of the module. But, we can start from the hook and improve later.

3. Cache should already be handled by the builder with no need for you to add extra settings, but do tell me if you experience cache trouble.

4. I'd love to use a Class just so someone else might be able to extend your work later the same way you were able to call >build rather than the D7 way of adding hooks to our hooks.

5. I have no strong opinions here. Anyone else?

~G

Greg Boggs’s picture

Can you post LD output in a code block so I Can validate? I know I could download the patch, etc, but I don't have a working D8 site set up. So, having the output from your test site would be super handy.

~G

leymannx’s picture

I will respond more detailed later or maybe even send an updated patch. I'll also try to get an opinion about this whole structured data breadcrumb thing from an SEO expert to be included here.

The markup currently looks like following.

1. Front page

<script type="application/ld+json">{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [{
    "@type": "ListItem",
    "position": "1",
    "name": "Home",
    "item": "http://example.localhost/"
  }]}</script>

2. Sub page

<script type="application/ld+json">{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [{
    "@type": "ListItem",
    "position": "1",
    "name": "Home",
    "item": "http://example.localhost/"
  },{
    "@type": "ListItem",
    "position": "2",
    "name": "Au pair USA",
    "item": "http://example.localhost/au-pair-usa"
  }]}</script>

3. Sub sub page

<script type="application/ld+json">{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [{
    "@type": "ListItem",
    "position": "1",
    "name": "Home",
    "item": "http://example.localhost/"
  },{
    "@type": "ListItem",
    "position": "2",
    "name": "Au pair USA",
    "item": "http://example.localhost/au-pair-usa"
  },{
    "@type": "ListItem",
    "position": "3",
    "name": "Start in New York",
    "item": "http://example.localhost/au-pair-usa/start-in-new-york"
  }]}</script>
Greg Boggs’s picture

Looks good.

leymannx’s picture

Uhkay, found out some things now. First and most important thing is stated in Googles guidelines to structured data.

Don't mark up content that is not visible to readers of the page. For example, if the JSON-LD markup describes a performer, the HTML body should describe that same performer.

Which means we actually must print the structured breadcrumb data only when there really are breadcrumbs visible on the page. That's changing the specs quite a bit. Which also makes it necessary to get the same output (out of the build or from somewhere else) for the JSON-LD as the actual breadcrumb block is getting.

This makes me think that we probably are better off placing the JSON-LD directly in the block and not in the <head>.

leymannx’s picture

FileSize
5.49 KB

Okay, next try.

1. Since the structured data is bound to the block (only add structured data for content which is being shown on the page you add it to) I removed the extra settings form and append the checkbox to enable structured data to the existing settings form (under the advanced section).

2. From the same reason I now use hook_block_view_BASE_BLOCK_ID_alter to attach the structured data directly to the block's $build (when the block appears on the page), from where it bubbles up into the HTML head. No block on the page, no structured data on the page.

3. I added a new class EasyBreadcrumbStructuredDataJsonLd class and a new service for that class, and therefore a new update hook to have the caches flushed explicitly (an empty update hook seemed to be not enough) for the service to be picked up correctly.

4. I removed the superfluous cache context from the JSON attachment.

TODOs

- Drupal calls should be avoided in the new class, use dependency injection instead
- Add tests
- Update README
- Update module description here on drupal.org

leymannx’s picture

I'd be more than happy if someone could assist in the dependency injection and adding tests task.

leymannx’s picture

FileSize
7.38 KB

Oopsie, newly added class file was missing.

leymannx’s picture

Forgot to mention: The build method returns the links absolutely fine. I forgot that I head the breadcrumb block hidden from the front page per block visibility settings instead of Easy Breadcrumb settings. So no bug in there. :)

leymannx’s picture

Status: Needs review » Needs work
leymannx’s picture

leymannx’s picture

Gosh, don't know how this is possible. I've submitted an outdated comment form and now can't unhide the uploaded patches. Reuploading last patch from #29.

leymannx’s picture

FileSize
8.77 KB

Sooooo, I made the EasyBreadcrumbStructuredDataJsonLd.php class use dependency injection now.

Last bigger task left now is to add tests for checking if the structured data can be found in the markup/head or not. And I really need help on this one. Maybe theoretically first.

I think we should create some nested nodes (or can this be done just with routes?), with paths like /hello and /hello/world. Maybe just a HelloWordController.php that takes its title ("Hello" or "World") from path arguments? And then assert the structured data exists on the page or not, depending on the current add_structured_data_json_ld setting? We could also check if the structured data exists on the page depending on home_segment_keep or hide_single_home_item setting. What do you think? Has anyone of us some experience with writing these kind of tests?

leymannx’s picture

Issue summary: View changes
Summit’s picture

Hi, Is this adding of structured data also possible for Drupal 7 please?
Thanks a lot in advance for your reply!
greetings, Martijn

leymannx’s picture

skouf’s picture

Hello

I have Drupal 8 and I installed the patch structured-data_2799067_34.patch.

I checked the checkbox inside the easybreadcrumb module to enable structured datas, emptied my cache, but ht code remain unchanged.

What am I missing ?

Thanks

leymannx’s picture

You need to place the breadcrumb block on the page. And it should print breadcrumbs. No block with breadcrumbs, no structured data.

skouf’s picture

FileSize
27.77 KB

Thanks for your reply :)

I have my breadcrumb already on my page.html.twig ({{ page.breadcrumb }})

My block System breadcrumb is enabled on the admin block page

Still no structured data :(

leymannx’s picture

Please check your HTML <head> for <script type="application/ld+json">.

skouf’s picture

Yes I can see that :

{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{ "@type": "ListItem", "position": "1", "name": "Accueil", "item": "http://site-url.com/fr" },{ "@type": "ListItem", "position": "2", "name": "Groupe", "item": "http://site-url.com/fr/page" }]}

But tags are not added inside the breadcrumb HTML

leymannx’s picture

As mentioned in #27 I used hook_block_view_alter to attach the structured data to the block build from where it then bubbles up into the HTML <head>. It would have been no good idea to put the markup into the block template. Since people override templates. And I think it also was difficult if not impossible to simply append a unfiltered <script> tag to the block's markup.

leymannx’s picture

Last comment updated to match the accomplished progress.

skouf’s picture

Thanks for your help

So I have to create sort of javascript function to put each element of your hook inside the correct tags ?

leymannx’s picture

You don't have to do anything. You already have everything. Structured data in the HTML head. Google will appreciate that and soon you'll see the result on Google's SERPs.

  • Greg Boggs committed 1b68813 on 8.x-1.x authored by leymannx
    Issue #2799067 by leymannx, skouf, kurtismccartney: Structured data
    
Greg Boggs’s picture

Status: Needs review » Fixed

Thanks!

leymannx’s picture

🎉 How cool is that?

Greg, do you get notified about my comment on GitLab? https://git.drupalcode.org/project/easy_breadcrumb/-/commit/1b68813ee9cd...

(Just asking, as this is the first time I commented code on the Drupal GitLab and don't know if this triggers something or not.)

Greg Boggs’s picture

Nope! Comments on Gitlab seem to go nowhere. We were also missing the submitter for the LD setting. So, I rewrote the form submission for settings to be dynamic.

It looks like the current page crumb is also missing from the LD which is in the example provided by Google:

https://developers.google.com/search/docs/data-types/breadcrumb#json-ld

Greg Boggs’s picture

Feel free to review again. I updated the code a bit and pushed it all to the dev branch.

Status: Fixed » Closed (fixed)

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