Support for Yoast plugin REST API fields

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)) {
  //...
}

Right, I see your point.

I implemented state.source.entity() this way because for me it wasn’t clear how to know the type of an entity depending on the given link but I didn’t realize it is the same case as state.source.data!

Using type guards here it’s a good idea. :+1:

By the way, would it be a good idea to add a warning when the yoast_head tag is not found?

In case people have not installed Yoast or the version is lower than 14…

It makes sense to me. Maybe also do the opposite in the head-tags package? If yoast_head is found we could recommend to use this package instead.