Skip to content

Commit

Permalink
Add support for creating non-meta tags with v2_meta (#5746)
Browse files Browse the repository at this point in the history
  • Loading branch information
chaance committed Mar 10, 2023
1 parent 33787d8 commit 1d46d33
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/meta-v2-enhancements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@remix-run/react": minor
"@remix-run/server-runtime": patch
---

Add support for generating `<script type='application/ld+json' />` and meta-related `<link />` tags to document head via the route `meta` function when using the `v2_meta` future flag
91 changes: 88 additions & 3 deletions integration/meta-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,9 @@ test.describe("v2_meta", () => {
`,

"app/routes/_index.jsx": js`
export const meta = ({ data, matches }) => [
...matches.map((match) => match.meta),
];
export const meta = ({ data, matches }) =>
matches.flatMap((match) => match.meta);
export default function Index() {
return <div>This is the index file</div>;
}
Expand All @@ -464,6 +464,59 @@ test.describe("v2_meta", () => {
}
`,

"app/routes/authors.$authorId.jsx": js`
import { json } from "@remix-run/node";
export async function loader({ params }) {
return json({
author: {
id: params.authorId,
name: "Sonny Day",
address: {
streetAddress: "123 Sunset Cliffs Blvd",
city: "San Diego",
state: "CA",
zip: "92107",
},
emails: [
"sonnyday@fancymail.com",
"surfergal@veryprofessional.org",
],
},
});
}
export function meta({ data }) {
let { author } = data;
return [
{ title: data.name + " Profile" },
{
tagName: "link",
rel: "canonical",
href: "https://website.com/authors/" + author.id,
},
{
"script:ld+json": {
"@context": "http://schema.org",
"@type": "Person",
"name": author.name,
"address": {
"@type": "PostalAddress",
"streetAddress": author.address.streetAddress,
"addressLocality": author.address.city,
"addressRegion": author.address.state,
"postalCode": author.address.zip,
},
"email": author.emails,
},
},
];
}
export default function AuthorBio() {
return <div>Bio here!</div>;
}
`,

"app/routes/music.jsx": js`
export function meta({ data, matches }) {
let rootModule = matches.find(match => match.route.id === "root");
Expand Down Expand Up @@ -531,4 +584,36 @@ test.describe("v2_meta", () => {
await app.goto("/");
expect(await app.getHtml('meta[property="og:image"]')).toBeTruthy();
});

test("{ 'script:ld+json': {} } adds a <script type='application/ld+json' />", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/authors/1");
let scriptTag = await app.getHtml('script[type="application/ld+json"]');
let scriptContents = scriptTag
.replace('<script type="application/ld+json">', "")
.replace("</script>", "")
.trim();

expect(JSON.parse(scriptContents)).toEqual({
"@context": "http://schema.org",
"@type": "Person",
name: "Sonny Day",
address: {
"@type": "PostalAddress",
streetAddress: "123 Sunset Cliffs Blvd",
addressLocality: "San Diego",
addressRegion: "CA",
postalCode: "92107",
},
email: ["sonnyday@fancymail.com", "surfergal@veryprofessional.org"],
});
});

test("{ tagName: 'link' } adds a <link />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/authors/1");
expect(await app.getHtml('link[rel="canonical"]')).toBeTruthy();
});
});
51 changes: 43 additions & 8 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ function V1Meta() {
}

if (["charset", "charSet"].includes(name)) {
return <meta key="charset" charSet={value as string} />;
return <meta key="charSet" charSet={value as string} />;
}

if (name === "title") {
Expand Down Expand Up @@ -719,18 +719,49 @@ function V2Meta() {
return null;
}

if ("tagName" in metaProps) {
let tagName = metaProps.tagName;
delete metaProps.tagName;
if (!isValidMetaTag(tagName)) {
console.warn(
`A meta object uses an invalid tagName: ${tagName}. Expected either 'link' or 'meta'`
);
return null;
}
let Comp = tagName;
return <Comp key={JSON.stringify(metaProps)} {...metaProps} />;
}

if ("title" in metaProps) {
return <title key="title">{String(metaProps.title)}</title>;
}

if ("charSet" in metaProps || "charset" in metaProps) {
// TODO: We normalize this for the user in v1, but should we continue
// to do that? Seems like a nice convenience IMO.
if ("charset" in metaProps) {
metaProps.charSet ??= metaProps.charset;
delete metaProps.charset;
}

if ("charSet" in metaProps && metaProps.charSet != null) {
return typeof metaProps.charSet === "string" ? (
<meta key="charSet" charSet={metaProps.charSet} />
) : null;
}

if ("script:ld+json" in metaProps) {
let json: string | null = null;
try {
json = JSON.stringify(metaProps["script:ld+json"]);
} catch (err) {}
return (
<meta
key="charset"
charSet={metaProps.charSet || (metaProps as any).charset}
/>
json != null && (
<script
key="script:ld+json"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(metaProps["script:ld+json"]),
}}
/>
)
);
}
return <meta key={JSON.stringify(metaProps)} {...metaProps} />;
Expand All @@ -739,6 +770,10 @@ function V2Meta() {
);
}

function isValidMetaTag(tagName: unknown): tagName is "meta" | "link" {
return typeof tagName === "string" && /^(meta|link)$/.test(tagName);
}

export function Meta() {
let { future } = useRemixContext();
return future?.v2_meta ? <V2Meta /> : <V1Meta />;
Expand Down
11 changes: 10 additions & 1 deletion packages/remix-react/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,16 @@ export type V2_HtmlMetaDescriptor =
| { name: string; content: string }
| { property: string; content: string }
| { httpEquiv: string; content: string }
| { [name: string]: string };
| { "script:ld+json": LdJsonObject }
| { tagName: "meta" | "link"; [name: string]: string }
| { [name: string]: unknown };

type LdJsonObject = { [Key in string]: LdJsonValue } & {
[Key in string]?: LdJsonValue | undefined;
};
type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];
type LdJsonPrimitive = string | number | boolean | null;
type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;

/**
* A React component that is rendered for a route.
Expand Down
11 changes: 10 additions & 1 deletion packages/remix-server-runtime/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,16 @@ export type V2_HtmlMetaDescriptor =
| { name: string; content: string }
| { property: string; content: string }
| { httpEquiv: string; content: string }
| { [name: string]: string };
| { "script:ld+json": LdJsonObject }
| { tagName: "meta" | "link"; [name: string]: string }
| { [name: string]: unknown };

type LdJsonObject = { [Key in string]: LdJsonValue } & {
[Key in string]?: LdJsonValue | undefined;
};
type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];
type LdJsonPrimitive = string | number | boolean | null;
type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;

/**
* A React component that is rendered for a route.
Expand Down

0 comments on commit 1d46d33

Please sign in to comment.