Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(remix-react): fix attributes for namespaced Open Graph and Facebook meta tags #4445

Merged
merged 3 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-hornets-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/react": patch
---

Make sure namespaced Open Graph and `fb:app_id` meta data renders the correct attributes on `<meta>` tags
275 changes: 269 additions & 6 deletions integration/meta-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,197 @@ test.describe("meta", () => {
return <div>This is the index file</div>;
}
`,

"app/routes/music.jsx": js`
export function meta({ data }) {
return {
title: "What's My Age Again?",
"og:type": "music.song",
"music:musician": "https://www.blink182.com/",
"music:duration": 182,
};
}

export default function Music() {
return <h1>Music</h1>;
}
`,

"app/routes/video.jsx": js`
export function meta({ data }) {
return {
title: "Catch Me If You Can",
"og:type": "video.movie",
"video:actor": "Leonardo DiCaprio",
"video:actor:role": "Frank Abagnale Jr.",
"video:director": "Steven Spielberg",
};
}

export default function Video() {
return <h1>Video</h1>;
}
`,

"app/routes/book.jsx": js`
export function meta({ data }) {
return {
title: "The Hitchhiker's Guide to the Galaxy",
"og:type": "book",
"book:author": "Douglas Adams",
"book:isbn": "0345391802",
};
}

export default function Book() {
return <h1>Book</h1>;
}
`,

"app/routes/profile.jsx": js`
export function meta({ data }) {
return {
title: "Chance's Profile",
"og:type": "profile",
"profile:first_name": "Chance",
"profile:last_name": "Strickland",
"profile:username": "chancethedev",
};
}

export default function Profile() {
return <h1>Profile</h1>;
}
`,

"app/routes/fb.jsx": js`
export function meta({ data }) {
return {
"fb:app_id": "54321",
};
}

export default function FB() {
return <h1>FB App</h1>;
}
`,

"app/routes/twitter.jsx": js`
export function meta({ data }) {
return {
"twitter:site": "@chancethedev",
};
}

export default function Twitter() {
return <h1>Twitter App</h1>;
}
`,

"app/routes/bogus.jsx": js`
export function meta({ data }) {
return {
title: "Bogus page",
"bogus:value": "Whatever man",
};
}

export default function Profile() {
return <h1>Profile</h1>;
}
`,

"app/routes/blog.jsx": js`
import { Outlet } from "@remix-run/react";

export const meta = ({ data }) => ({
title: "Blog",
description: "The best blog on earth",
"og:image": "https://picsum.photos/300/300",
"og:type": "article",
"article:author": ["Logan McAnsh", "Chance Strickland"],
});

export default function BlogLayout() {
return (
<div>
<h1>Blog</h1>
<Outlet />
</div>
);
}
`,

"app/routes/blog/index.jsx": js`
import { Link, useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";

const posts = [
{ id: 1, title: "Post 1", content: "This is post 1" },
{ id: 2, title: "Post 2", content: "This is post 2", author: "Ryan Florence" },
{ id: 3, title: "Post 3", content: "This is post 3" },
];

export const loader = async () => json({ posts });

export function meta({ data }) {
return {
title: "Blog Posts",
};
}

export default function BlogIndex() {
let { posts } = useLoaderData();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link to={"/blog/" + post.id}>{post.title}</Link>
</li>
))}
</ul>
);
}
`,

"app/routes/blog/$pid.jsx": js`
import { useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";

const posts = [
{ id: 1, title: "Post 1", content: "This is post 1" },
{ id: 2, title: "Post 2", content: "This is post 2", author: "Ryan Florence" },
{ id: 3, title: "Post 3", content: "This is post 3" },
];

export async function loader({ params }) {
let post = posts.find((post) => post.id === Number(params.pid));
if (!post) {
throw json(null, 404);
}
return json(post);
}

export function meta({ data }) {
let meta = {
title: data.title + " | Blog",
};
if (data.author) {
meta["article:author"] = data.author;
}
return meta;
}

export default function BlogPost() {
let post = useLoaderData();
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
`,
},
});

Expand All @@ -76,28 +267,73 @@ test.describe("meta", () => {
);
});

test("meta { charset } adds a <meta charset='utf-8' />", async ({ page }) => {
test("{ charset } adds a <meta charset='utf-8' />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");

expect(await app.getHtml('meta[charset="utf-8"]')).toBeTruthy();
});

test("meta { title } adds a <title />", async ({ page }) => {
test("{ title } adds a <title />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");

expect(await app.getHtml("title")).toBeTruthy();
});

test("meta { 'og:*' } adds a <meta property='og:*' />", async ({ page }) => {
test("{ 'og:*' } adds a <meta property='og:*' />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");

expect(await app.getHtml('meta[property="og:image"]')).toBeTruthy();
});

test("meta { description } adds a <meta name='description' />", async ({
test("{ 'music:*' } adds a <meta property='music:*' />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/music");
expect(await app.getHtml('meta[property="music:musician"]')).toBeTruthy();
});

test("{ 'video:*' } adds a <meta property='video:*' />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/video");
expect(await app.getHtml('meta[property="video:actor"]')).toBeTruthy();
});

test("{ 'book:*' } adds a <meta property='book:*' />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/book");
expect(await app.getHtml('meta[property="book:author"]')).toBeTruthy();
});

test("{ 'profile:*' } adds a <meta property='profile:*' />", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/profile");
expect(await app.getHtml('meta[property="profile:username"]')).toBeTruthy();
});

test("{ 'fb:*' } adds a <meta property='fb:*' />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/fb");
expect(await app.getHtml('meta[property="fb:app_id"]')).toBeTruthy();
});

test("{ 'twitter:*' } adds a <meta name='twitter:*' />", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/twitter");
expect(await app.getHtml('meta[name="twitter:site"]')).toBeTruthy();
});

test("{ 'article:*' } adds a <meta property='article:*' />", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/blog");
expect(await app.getHtml('meta[property="article:author"]')).toBeTruthy();
});

test("{ description } adds a <meta name='description' />", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
Expand All @@ -106,7 +342,7 @@ test.describe("meta", () => {
expect(await app.getHtml('meta[name="description"]')).toBeTruthy();
});

test("meta { refresh } adds a <meta http-equiv='refresh' content='3;url=https://www.mozilla.org' />", async ({
test("{ refresh } adds a <meta http-equiv='refresh' content='3;url=https://www.mozilla.org' />", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
Expand All @@ -118,4 +354,31 @@ test.describe("meta", () => {
)
).toBeTruthy();
});

test("arbitrary key with : adds a <meta name='[VALUE]' />", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/bogus");
expect(await app.getHtml('meta[name="bogus:value"]')).toBeTruthy();
});

test.describe("in nested routes", () => {
test("meta from layout routes are inherited", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/blog");

expect(
await app.getHtml(
'meta[name="description"][content="The best blog on earth"]'
)
).toBeTruthy();
});

test("meta from layout routes can be overridden", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/blog");
expect(await app.getHtml("title")).toBe("<title>Blog Posts</title>");
});
});
});
17 changes: 16 additions & 1 deletion packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,22 @@ export function Meta() {

// Open Graph tags use the `property` attribute, while other meta tags
// use `name`. See https://ogp.me/
let isOpenGraphTag = name.startsWith("og:");
//
// Namespaced attributes:
// - https://ogp.me/#type_music
// - https://ogp.me/#type_video
// - https://ogp.me/#type_article
// - https://ogp.me/#type_book
// - https://ogp.me/#type_profile
//
// Facebook specific tags begin with `fb:` and also use the `property`
// attribute.
//
// Twitter specific tags begin with `twitter:` but they use `name`, so
// they are excluded.
let isOpenGraphTag =
/^(og|music|video|article|book|profile|fb):.+$/.test(name);

return [value].flat().map((content) => {
if (isOpenGraphTag) {
return (
Expand Down