Skip to content

Commit

Permalink
Deprecate the Seo component and add getSeoMeta for migration (#1875)
Browse files Browse the repository at this point in the history
* Deprecate the Seo component and add `getSeoMeta` for migration
  • Loading branch information
blittle committed Apr 9, 2024
1 parent be1a79b commit 4afedb4
Show file tree
Hide file tree
Showing 13 changed files with 1,794 additions and 37 deletions.
106 changes: 106 additions & 0 deletions .changeset/tiny-sheep-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
'@shopify/hydrogen': patch
---

Deprecate the `<Seo />` component in favor of directly using Remix meta route exports. Add the `getSeoMeta` to make migration easier:

Migration steps:

**1. Remove the `<Seo />` component from `root.jsx`:**

```diff
export default function App() {
const nonce = useNonce();
const data = useLoaderData<typeof loader>();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
- <Seo />
<Meta />
<Links />
</head>
<body>
<Layout {...data}>
<Outlet />
</Layout>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}

```

**2. Add a Remix meta export to each route that returns an `seo` property from a `loader` or `handle`:**

```diff
+import {getSeoMeta} from '@shopify/hydrogen';

export async function loader({context}) {
const {shop} = await context.storefront.query(`
query layout {
shop {
name
description
}
}
`);

return {
seo: {
title: shop.title,
description: shop.description,
},
};
}

+export const meta = ({data}) => {
+ return getSeoMeta(data.seo);
+};
```

**3. Merge root route meta data**

If your root route loader also returns an `seo` property, make sure to merge that data:

```ts
export const meta = ({data, matches}) => {
return getSeoMeta(
matches[0].data.seo,
// the current route seo data overrides the root route data
data.seo,
);
};
```

Or more simply:

```ts
export const meta = ({data, matches}) => {
return getSeoMeta(...matches.map((match) => match.data.seo));
};
```

**4. Override meta**

Sometimes `getSeoMeta` might produce a property in a way you'd like to change. Map over the resulting array to change it. For example, Hydrogen removes query parameters from canonical URLs, add them back:

```ts
export const meta = ({data, location}) => {
return getSeoMeta(data.seo).map((meta) => {
if (meta.rel === 'canonical') {
return {
...meta,
href: meta.href + location.search,
};
}

return meta;
});
};
```
429 changes: 413 additions & 16 deletions packages/hydrogen/docs/generated/generated_docs_data.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/hydrogen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {type CacheKey} from './cache/fetch';
export {storefrontRedirect} from './routing/redirect';
export {graphiqlLoader} from './routing/graphiql';
export {Seo} from './seo/seo';
export {getSeoMeta} from './seo/getSeoMeta';
export {type SeoConfig} from './seo/generate-seo-tags';
export type {SeoHandleFunction} from './seo/seo';
export {Pagination, getPaginationVariables} from './pagination/Pagination';
Expand Down
8 changes: 4 additions & 4 deletions packages/hydrogen/src/seo/generate-seo-tags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ describe('generateSeoTags', () => {
// Given
const input = {
jsonLd: {},
} as SeoConfig<Thing>;
} as SeoConfig;

// When
const output = generateSeoTags(input);
Expand All @@ -800,7 +800,7 @@ describe('generateSeoTags', () => {
],
url: 'http://localhost:3000/products/the-full-stack',
},
} satisfies SeoConfig<Organization>;
} satisfies SeoConfig;

// When
const output = generateSeoTags(input);
Expand Down Expand Up @@ -845,7 +845,7 @@ describe('generateSeoTags', () => {
],
},
},
} satisfies SeoConfig<Product>;
} satisfies SeoConfig;

// When
const output = generateSeoTags(input);
Expand Down Expand Up @@ -907,7 +907,7 @@ describe('generateSeoTags', () => {
},
},
],
} satisfies SeoConfig<Organization | Product>;
} satisfies SeoConfig;

// When
const output = generateSeoTags(input);
Expand Down
21 changes: 10 additions & 11 deletions packages/hydrogen/src/seo/generate-seo-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ export const schema = {
},
};

export interface SeoConfig<Schema extends Thing = Thing> {
export interface SeoConfig {
/**
* The <title> HTML element defines the document's title that is shown in a browser's title bar or a page's tab. It
* The `title` HTML element defines the document's title that is shown in a browser's title bar or a page's tab. It
* only contains text; tags within the element are ignored.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title
Expand Down Expand Up @@ -159,6 +159,8 @@ export interface SeoConfig<Schema extends Thing = Thing> {
* - `BlogPosting`
* - `Thing`
*
* The value is validated via [schema-dts](https://www.npmjs.com/package/schema-dts)
*
* @example
* ```js
* {
Expand Down Expand Up @@ -200,7 +202,7 @@ export interface SeoConfig<Schema extends Thing = Thing> {
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
*
*/
jsonLd?: WithContext<Schema> | WithContext<Schema>[];
jsonLd?: WithContext<Thing> | WithContext<Thing>[];
/**
* The `alternates` property is used to specify the language and geographical targeting when you have multiple
* versions of the same page in different languages. The `url` property tells search engines about these variations
Expand Down Expand Up @@ -354,10 +356,7 @@ export interface CustomHeadTagObject {
* pairs well with the SEO component in `@shopify/hydrogen` when building a Hydrogen Remix app, but can be used on its
* own if you want to generate the tags yourself.
*/
export function generateSeoTags<
Schema extends Thing,
T extends SeoConfig<Schema> = SeoConfig<Schema>,
>(seoInput: T): CustomHeadTagObject[] {
export function generateSeoTags(seoInput: SeoConfig): CustomHeadTagObject[] {
const tagResults: CustomHeadTagObject[] = [];

for (const seoKey of Object.keys(seoInput)) {
Expand Down Expand Up @@ -685,7 +684,7 @@ export function generateKey(tag: CustomHeadTagObject, group?: string) {
return `${tagName}-${props.type}`;
}

function renderTitle<T extends CustomHeadTagObject['children']>(
export function renderTitle<T extends CustomHeadTagObject['children']>(
template?:
| string
| ((title: string) => string | undefined)
Expand All @@ -708,7 +707,7 @@ function renderTitle<T extends CustomHeadTagObject['children']>(
return template.replace('%s', title ?? '');
}

function inferMimeType(url: Maybe<string> | undefined) {
export function inferMimeType(url: Maybe<string> | undefined) {
const ext = url && url.split('.').pop();

switch (ext) {
Expand Down Expand Up @@ -738,11 +737,11 @@ export type SchemaType =
| 'BlogPosting'
| 'Thing';

function ensureArray<T>(value: T | T[]): T[] {
export function ensureArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}

function validate<T>(
export function validate<T>(
schema: {validate: <T>(data: T) => NonNullable<T>},
data: T,
): T {
Expand Down
37 changes: 37 additions & 0 deletions packages/hydrogen/src/seo/getSeoMeta.doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';

const data: ReferenceEntityTemplateSchema = {
name: 'getSeoMeta',
category: 'utilities',
isVisualComponent: false,
related: [],
description: `Generate a [Remix meta array](https://remix.run/docs/en/main/route/meta) from one or more SEO configuration objects. Pass SEO configuration for the parent route(s) and the current route to preserve meta data for all active routes. Similar to [\`Object.assign()\`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign), each property is overwritten based on the object order. The exception is \`jsonLd\`, which is preserved so that each route has it's own independent jsonLd meta data. Learn more about [how SEO works in Hydrogen](https://shopify.dev/docs/custom-storefronts/hydrogen/seo).`,
type: 'utility',
defaultExample: {
description: 'I am the default example',
codeblock: {
tabs: [
{
title: 'JavaScript',
code: './getSeoMeta.example.jsx',
language: 'js',
},
{
title: 'TypeScript',
code: './getSeoMeta.example.tsx',
language: 'ts',
},
],
title: 'Example code',
},
},
definitions: [
{
title: 'getSeoMeta',
type: 'GetSeoMetaTypeForDocs',
description: '',
},
],
};

export default data;
24 changes: 24 additions & 0 deletions packages/hydrogen/src/seo/getSeoMeta.example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {getSeoMeta} from '@shopify/hydrogen';

export async function loader({context}) {
const {shop} = await context.storefront.query(`
query layout {
shop {
name
description
}
}
`);

return {
seo: {
title: shop.title,
description: shop.description,
},
};
}

export const meta = ({data, matches}) => {
// Pass one or more arguments, preserving properties from parent routes
return getSeoMeta(matches[0].data.seo, data.seo);
};
26 changes: 26 additions & 0 deletions packages/hydrogen/src/seo/getSeoMeta.example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {MetaFunction} from '@remix-run/react';
import {LoaderFunctionArgs} from '@remix-run/server-runtime';
import {getSeoMeta} from '@shopify/hydrogen';

export async function loader({context}: LoaderFunctionArgs) {
const {shop} = await context.storefront.query(`
query layout {
shop {
name
description
}
}
`);

return {
seo: {
title: shop.title,
description: shop.description,
},
};
}

export const meta: MetaFunction<typeof loader> = ({data, matches}) => {
// Pass one or more arguments, preserving properties from parent routes
return getSeoMeta((matches as any).data.seo, data!.seo);
};
Loading

0 comments on commit 4afedb4

Please sign in to comment.