Support for Yoast plugin REST API fields

OPENING POST


npm package

Description

Since version 14.0, Yoast added a REST API field where it shows the head tags needed for SEO purposes, so it’d be great to have a package similar to @frontity/head-tags that fetch the head tags from Yoast field instead of from our plugin.

This way, Yoast team would take care of the REST API support (as they are already doing) and we just have to take care of adapting that info for Frontity with this package.

With this solution, if you use Yoast ^14.0 (the version where they added the REST API support), you could use @frontity/yoast package and, if you use an older version of the plugin, or any other plugin without API support, you can use the Head Tags plugin & @frontity/head-tags solution.

Although the Head Tags plugin is working great, users have to install another plugin, so this would mean one plugin less, just with Yoast is enough.

Possible solution

This package could be pretty similar to the @frontity/head-tags package, as it’s going to do the same, but getting the info that the Yoast plugin is showing in the REST API.

We had another conversation about how it works at this topic. The Yoast plugin is adding a new field to the REST API named yoast_head , which is a string of all the meta tags (it’s similar to what we’re doing with the REST API Head Tags plugin , but we are adding an array instead of a string). This way, if you go to a post, a category, a tag, etc, in the REST API, you can find this new field with all the meta tag of the specific entity. So if I go to https://myweb.com/wp-json/wp/v2/posts?slug=my-post , I can find this string:
Screenshot from 2020-04-28 16-53-40

Apart from this, in the WordPress side, we would need to add the types endpoint to _links with embeddable: true to make sure that information is retrieved in a single fetch when you request a post-type archive using ?_embed=true . This how we do it in the head-tags plugin. The Yoast team has opened this ticket to add it to WordPress core, but we could add it manually meanwhile or in the future Frontity plugin.


SUMMARY


:warning: Please, bear in mind that this section wasn’t part of the opening post. It has been added afterwards and its purpose is to keep a quick summary with the most relevant information about this FD at the top of the thread.

Final implementation

Relevant information

  • Demo.
  • In order to embed the post types – which are the entities that contain the yoast_meta field for post type archives (i.e. the homepage or the archive of a custom post type) – in the REST API responses, users should add a PHP Snippet as explained here.
  • This package only applies for Yoast ^14.0.0. For previous version take a look at the Head Tags plugin.

It is a weird problem that the yoast_head field is a string. I did some tests using the <Head> component and it can’t render strings directly, they must be converted first to React elements. Also, only valid elements can be nested into the <Head> component so it is not possible to add the <Html2react> component inside to convert the string, even though it would render valid meta tags.

My idea would be to use the <Html2react> component directly, add some processors that would recognise the meta tags and nest them into the <Head> component one by one.

What if we only extract the <title> in the client? I mean, are the other fields useful at all in the client-side render? I don’t think so :thinking:

That way we could avoid all the CPU cycles required to do the Html2React processing in the client.

I’ll ask Yoast if they can return an array.

I’ll ask Christian how important is to keep changing the head tags once the app has been hydrated.

Here to say that I too am interested in this issue, as one of my clients is already using Yoast 14.x and the current results of @frontity/head-tags do not match with the content of the field yoast_head.

Well, that’s something we can do, I guess. We can detect if it is SSR or not, and depending on the case render the <Html2React> component or just the <Head> component with a single <title> tag.

My only concern about this is if something like this would work fine

<Head>
  <title
    dangerouslySetInnerHTML={{ __html: extractTitle(entity.yoast_head) }}
  />
</Head>

or we would have to unescape the title string

<Head>
  <title>{unescape(extractTitle(entity.yoast_head))}</title>
</Head>

I think we created a sort of unescape function somewhere but I don’t remember which package contains it.

If Yoast finally return the head tags in JSON format, it would be great if the strings returned are unescaped. You can tell them that as well.

Hey @christianoliveira, we need your help here :slightly_smiling_face:

How important is, for a server-side rendered app like Frontity, to keep changing the head tags once the HTML has been loaded and the JavaScript has taken control of the page?

I understand that <title> needs to be updated each time there is a client navigation inside the app because that can be seen by the user, but is it important to update the other tags?

Hi @luisherranz !

Short answer: it’s ok if the head tags are not updated on non-first interactions (at least by now)

Long answer: In terms of SEO, in theory, it doesn’t matter, as Googlebot won’t generate a interaction like that (it will ask for a specific URL to the server, get a response - in your case, the rendered HTML - and it may or may not render that specific page, but, again, in theory, won’t navigate to another URL clicking on a link; it will just send another request directly to the server for any of the other URLs discovered on the site)

I remark the “in theory” part as this may change anytime, so if possible, the html should always correspond to the URL the user is visiting.

Let me know if this solves your doubt or if you need more info!

Thanks @christianoliveira, that’s really helpful.

David is going to take a look to see if we can provide both options :+1:

1 Like

Final Implementation

This package was rebuilt using a similar approach as the @frontity/head-tags package. It uses the yoast_head field included in the entities that come from the WordPress REST API and converts it to React.

The main problem here was that the content of that field is an HTML string with the head tags, and therefore it should be parsed and converted to React – using the Html2React package – before rendering the tags inside a <Head> component.

To solve that, the Html2React was modified to accept a new parameter called processors, which accepts an array of processors that overwrite the one stored in libraries.html2react.processors. The new parameter was used by the roots.yoast component to add a single processor that wraps all the head tags processed from the yoast_head field.

Package API

state.yoast.renderTags

Render the Yoast meta tags in both the server and the client or only in the server.

The title tag is still shown while navigating but not the rest of them.
This option is useful to improve the perfomance a bit as the
<Html2React> component is not used to render the title tag.

With this option active, the Yoast meta tags will only be
visible if you go to the page source, before Frontity has been loaded. It shouldn’t affect the SEO as Google does not do client-side navigation.

Default value: "both"

renderTags?: "both" | "server";

state.yoast.transformLinks

Define a set of properties to transform links present in the
yoast_head field in case you are
using Frontity in decoupled mode.

If you are using Frontity in embedded mode, this property must be set
to false.

{
  ignore: "^(wp-(json|admin|content|includes))|feed|comments|xmlrpc",
  base: "https://wp.mysite.com"
}
  • ignore (string, optional)

    RegExp in string format that defines a set of links that must
    not be transformed.

    Default value:

    "^(wp-(json|admin|content|includes))|feed|comments|xmlrpc",
    
  • base (string, optional)

    WordPress URL base that must be replaced by the Frontity URL
    base (specified in state.frontity.url). If this value is not
    set, it is computed from state.source.api.

@mburridge (or anyone else that wants to give their opinion) quick check: does state.yoast.onlyInSSR sounds right in English?

This is a setting for the @frontity/yoast package, that adds the head tags generated by Yoast only to the server side rendered HTML (but not to the client rendering). It’s for performance reasons, although it’s deactivated by default.

It could be other things like state.yoast.SRROnly, state.yoast.rendering: "server"… Or even mention the client if that makes more sense: state.yoast.clientRendering: false, state.yoast.skipClient: true, state.yoast.skipCSR and so on…

It sounds right to me but I would still suggest a slightly different naming! :slight_smile:

The name onlyInSSR describes how it works but not why you would want to use it. I would rather call it something like optimize.

Let’s say that I’m a user who is concerned about performance. I would then want to use that option, but how would I find it? The name onlyInSSR is not going to be very meaningful to me and I might even overlook it unless I read the whole description. On the other hand if we call it something like optimize the purpose should be clear.

Of course, we have to explain that the trade-off is that he tags won’t render client-side.

1 Like

How about useSSRoptimisation?

Hmm… not sure if it’d be confusing because we’re not adding any optimization to the SSR, we are just removing the CSR part to save some CPU cycles in the client hydration.

After a quick discussion on the standup today we decided to name it state.yoast.renderTags with the default value of "both" and a possible value of "server" to avoid rendering the tags on the client.

EDIT: I have updated the implementation proposal.

We also need to make the getEntity function public, to allow sites that don’t use the normalized structure of Frontity to override it. We’re going to move it to libraries.source.getEntity as this is something related to source and other packages use it as well.

@david can you take care of these two changes? :slightly_smiling_face:

Working on that.

By the way, I’m going to try using a derived property instead of a library, I think it could have a better API, something like state.source.entity(link). It could be overwritten as well.

Changes are done. @luisherranz, you can review them whenever you can.

It’s interesting to notice that this is the first non-theme package that also depends on @frontity/html2react.

And in state.source.api and state.frontity.url being correctly populated. We should start noting those things.

We should add that to the docs. And maybe we have to start thinking about our dependency system :smile:

Unforunately, this is a bad API for TypeScript. We have the same problem than when we do:

const data = state.source.get(state.router.link);

if (data.isPostType) {
  //...
}

So if we want to maintain this API, we are going to need to apply the same APIs than we are going to use for the other case:

import { isPostType } from "@frontity/source";

const data = state.source.get(state.router.link);

if (isPostType(data)) {
  //...
}

Maybe we can use the very same functions for both cases, to avoid two separate APIs:

import { isPostType } from "@frontity/source";

const entity = state.source.entity(state.router.link);

if (isPostType(entity)) {
  //...
}

But we need something. Manual casting is not good enough if our goal is to have good TypeScript support in Frontity.

I guess we can distinguish between post types, taxonomies, authors and so on just by looking at their props, can’t we?


I’m thinking about approving the PR now and solve this in this issue, which is also in the current sprint: TypeScript 3.9 breaks @frontity/source types.

@david we can tomorrow about this.

Oh, my mistake, we can’t use the same functions because we need the arg is Type syntax and the types are different.

function isTaxonomy(data: Data): data is TaxonomyData {
  return (data as TaxonomyData).isTaxonomy === true;
}

function isTaxonomy(entity: Entity): entity is TaxonomyEntity {
  return !!(entity as TaxonomyEntity).taxonomy;
}

Then maybe we need to distinguish between those with different entry points:

import { isPostType } from "@frontity/source/data";

const data = state.source.get(state.router.link);

if (isPostType(data)) {
  //...
}
import { isPostType } from "@frontity/source/entity";

const entity = state.source.entity(state.router.link);

if (isPostType(entity)) {
  //...
}

Or different namings:

import { isPostTypeData } from "@frontity/source";

const data = state.source.get(state.router.link);

if (isPostTypeData(data)) {
  //...
}
import { isPostTypeEntity } from "@frontity/source";

const entity = state.source.entity(state.router.link);

if (isPostTypeEntity(entity)) {
  //...
}