Refactor SEO#370
Conversation
9ffbd80 to
925a909
Compare
925a909 to
2a65420
Compare
|
We detected some changes in |
|
|
||
| export {storefrontRedirect} from './routing/redirect'; | ||
| export {graphiqlLoader} from './routing/graphiql'; | ||
| export {inferStorefrontSeo} from './seo/seo'; |
There was a problem hiding this comment.
Note, this will probably move to storefront kit
There was a problem hiding this comment.
Honestly, I'm not in love with the name 🤔. We should bikeshed as a team.
blittle
left a comment
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
Any issues setting up vitest without vite?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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", | ||
| }, |
There was a problem hiding this comment.
Should there also be a meta name="twitter:image" element?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I don't know. If twitter reads the other, better to keep less meta tags than more ;)
There was a problem hiding this comment.
Ah I had the same question.
| const headTags = inferStorefrontSeo(seoConfig); | ||
|
|
||
| /* eslint-disable react/no-children-prop */ | ||
| const html = headTags.map((tag) => { |
There was a problem hiding this comment.
Nit picky, but I wonder if it instead could be just:
return React.createElement(tag, {key: tag.key, ...tag.props}, tag.children);
chaance
left a comment
There was a problem hiding this comment.
No opinion on the function name, but the config API looks solid to me 👍
| { | ||
| "key": "meta-twitter:card", | ||
| "props": { | ||
| "content": "summary_large_image", |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
Is there any extra processing done for the canonical URL? Stripping query strings, or checking to ensure that it has https://, etc.?
There was a problem hiding this comment.
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\\"}", |
There was a problem hiding this comment.
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", | ||
| }, |
There was a problem hiding this comment.
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\\"}", |
There was a problem hiding this comment.
Is there any way to specify which image should be the default one for the LDJSON?
There was a problem hiding this comment.
There isn't currently.
| { | ||
| "key": "script-application/ld+json", | ||
| "props": { | ||
| "children": "{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Thing\\"}", |
There was a problem hiding this comment.
MediaObject maybe? https://schema.org/MediaObject
| type: 'Organization', | ||
| pattern: /\/policies\/([^\/]+)/, | ||
| }, | ||
| ]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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
inferStorefrontSeofunction 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:
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,scriptetc...) 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-storewith anSeocomponent), 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
demo-store– I'm going to add all of the necessary SEO fields to thedemo-storeand showcase the relationship between this library and the SFAPI. This will help further prove out this abstraction.