Skip to content

Refactor SEO#370

Merged
cartogram merged 9 commits into
v0.x-2022-10from
seo-take-1million
Jan 20, 2023
Merged

Refactor SEO#370
cartogram merged 9 commits into
v0.x-2022-10from
seo-take-1million

Conversation

@cartogram
Copy link
Copy Markdown
Contributor

@cartogram cartogram commented Jan 18, 2023

This PR makes a number of improvements on our SEO abstraction by making the core module agnostic to any framework or implementation in Remix. This is done by exporting a single inferStorefrontSeo function that takes a config and returns an array of object representations for each head tag that should be added for a SEO-optimised storefront.

For example:

// given:
const output = inferStorefrontSeo({title: 'Some shop title'});

console.log(ouput)
// would produce:
// [
//   {
//     "children": "Snowdevil",
//     "key": "title",
//     "props": {},
//     "tag": "title",
//   },
//   {
//     "key": "meta-og:title",
//     "props": {
//       "content": "Snowdevil",
//       "property": "og:title",
//     },
//     "tag": "meta",
//   },
//   {
//     "key": "meta-twitter:title",
//     "props": {
//       "content": "Snowdevil",
//       "name": "twitter:title",
//     },
//     "tag": "meta",
//   },
//   ...etc

Why this approach

A main blocker for implementing SEO in h2 was working within the confines of how Remix manages head tags inside their page module API. Specifying the individual head tags is easy in Remix at first, but quickly becomes tedious when trying to articulate tags of various types (link, meta, script etc...) all at one; which is necessary for "Good" SEO.

Going one layer up and providing a utility that is only responsible for inferring an intermediate representation as shown above, provides users the flexibility to take this into their applications however suits them best. At first, users will be responsible for a thin wrapper around this module that adds the actual JSX or HTML markup to the head of their page (as I have done in the demo-store with an Seo component), though we may one day decide to provide "adapters" for Remix and other frameworks that do this. I think it is okay that this logic lives in user-land for now.

It is easy to take this further and build tooling (ie: a devtool/debugger) to help developers better understand the resulting head tags, and because this is now framework agnostic, I suggest we put this in the Storefront SDK rather than hydrogen itself. This would leave only the example implementation in the demo-store, but the remainder of the code would go in that repo if folks agree with me on this.

Other notes

Tests

I've used snapshots tests to quickly scaffold out tests for all the various config options. I normally am not a fan of snapshot testing, but this allowed me to move fast with a good initial group of tests. We may want to move away from this pattern and write more specific and deliberate assertions, but this is an easy thing to do later on.

Typescript

The types can be made much better here, and is something I plan to iterate on a lot. In the meantime, I am open to feedback and tips as reviewers look through the code.

Robots

I've completely removed the robots configuration from this library. At the moment, I think it doesn't really add anything to paper over the robots meta tags and users can add those separately using Remix meta or whatever. We can change our mind on this later if it becomes an issue. I think less is more with this stuff at first.

Next steps

  1. Docs – I've documented the types pretty well, but will supplement all of this with guides shortly.
  2. More detailed example demo-store – I'm going to add all of the necessary SEO fields to the demo-store and showcase the relationship between this library and the SFAPI. This will help further prove out this abstraction.

@github-actions github-actions Bot had a problem deploying to preview January 18, 2023 20:04 Failure
@github-actions github-actions Bot had a problem deploying to preview January 19, 2023 04:11 Failure
@github-actions github-actions Bot had a problem deploying to preview January 19, 2023 04:54 Failure
@cartogram cartogram marked this pull request as ready for review January 19, 2023 14:20
@github-actions
Copy link
Copy Markdown
Contributor

We detected some changes in packages/*/package.json or packages/*/src, and there are no updates in the .changeset.
If the changes are user-facing and should cause a version bump, run npm run changeset add to track your changes and include them in the next release CHANGELOG.
If you are making simple updates to examples or documentation, you do not need to add a changeset.


export {storefrontRedirect} from './routing/redirect';
export {graphiqlLoader} from './routing/graphiql';
export {inferStorefrontSeo} from './seo/seo';
Copy link
Copy Markdown
Contributor Author

@cartogram cartogram Jan 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, this will probably move to storefront kit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I'm not in love with the name 🤔. We should bikeshed as a team.

Copy link
Copy Markdown
Contributor

@blittle blittle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @cartogram! Just a few comments!

"@types/recursive-readdir": "^2.2.1"
"@types/recursive-readdir": "^2.2.1",
"@shopify/hydrogen-react": "*",
"vitest": "^0.27.2",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any issues setting up vitest without vite?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None 🙂 and if we pull this part into the SDK, it should all be set up already.


export {storefrontRedirect} from './routing/redirect';
export {graphiqlLoader} from './routing/graphiql';
export {inferStorefrontSeo} from './seo/seo';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I'm not in love with the name 🤔. We should bikeshed as a team.

it('should fill the description', () => {
// Given
const input = {
description: 'A headless storefront',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if there is any auto truncating of the description? Should there be? Or maybe a warning if it exceeds recommended max length?

"name": "twitter:card",
},
"tag": "meta",
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there also be a meta name="twitter:image" element?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand, twitter:image will read from og:image so I've omitted it here. If you think it is better to be explicit I can add it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. If twitter reads the other, better to keep less meta tags than more ;)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I had the same question.

const headTags = inferStorefrontSeo(seoConfig);

/* eslint-disable react/no-children-prop */
const html = headTags.map((tag) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit picky, but I wonder if it instead could be just:

return React.createElement(tag, {key: tag.key, ...tag.props}, tag.children);

Copy link
Copy Markdown

@chaance chaance left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No opinion on the function name, but the config API looks solid to me 👍

{
"key": "meta-twitter:card",
"props": {
"content": "summary_large_image",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, so the default choice is always the larger image rather than the summary card? Is there any way to change that if wanted?

"tag": "title",
},
{
"key": "meta-og:title",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn facebook trying to rebrand as meta and getting me confused on whether this is for "meta the company" or a "meta tag".

{
"key": "link-canonical",
"props": {
"href": "https://hydrogen.shop/collections",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any extra processing done for the canonical URL? Stripping query strings, or checking to ensure that it has https://, etc.?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we might want to "normalize" the URL before building this tag.

{
"key": "script-application/ld+json",
"props": {
"children": "{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"ItemList\\",\\"url\\":\\"https://hydrogen.shop/collections\\"}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this using ItemList because your function was smart enough to know it was a collection URL? Or is that the default?

[edit] I see below that it's smart enough. Nice.

"name": "twitter:card",
},
"tag": "meta",
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I had the same question.

{
"key": "script-application/ld+json",
"props": {
"children": "{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Thing\\",\\"image\\":\\"https://example.com/image-2.jpg\\"}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to specify which image should be the default one for the LDJSON?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't currently.

{
"key": "script-application/ld+json",
"props": {
"children": "{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Thing\\"}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MediaObject maybe? https://schema.org/MediaObject

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

type: 'Organization',
pattern: /\/policies\/([^\/]+)/,
},
];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, but also, this falls into the thinking that there are standardized URLs and that custom storefronts will follow them, no? Which is kind of the opposite of custom storefronts and being able to define your own URL structure.

What if we have a localized url? What if they use /prods for products? What if they use /product instead of /products? etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we try and assume defaults and they can override the value if they want. If this becomes an issue we can try and build on this inference or remove it all together.

@cartogram cartogram merged commit 1666232 into v0.x-2022-10 Jan 20, 2023
@cartogram cartogram deleted the seo-take-1million branch January 20, 2023 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants