diff --git a/__fixtures__/test-project/web/src/pages/AboutPage/AboutPage.tsx b/__fixtures__/test-project/web/src/pages/AboutPage/AboutPage.tsx
index a09fda799fb9..6428b03989d7 100644
--- a/__fixtures__/test-project/web/src/pages/AboutPage/AboutPage.tsx
+++ b/__fixtures__/test-project/web/src/pages/AboutPage/AboutPage.tsx
@@ -1,5 +1,5 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const AboutPage = () => {
return (
diff --git a/__fixtures__/test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx b/__fixtures__/test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
index bdc0a0cda146..415fbe886478 100644
--- a/__fixtures__/test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
+++ b/__fixtures__/test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
@@ -1,5 +1,5 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
type BlogPostPageProps = {
id: number
@@ -10,7 +10,7 @@ import BlogPostCell from 'src/components/BlogPostCell'
const BlogPostPage = ({ id }: BlogPostPageProps) => {
return (
<>
-
+
>
diff --git a/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx b/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
index aab8a211e64e..529d72d8bfbc 100644
--- a/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
+++ b/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
@@ -9,7 +9,7 @@ import {
Label,
} from '@redwoodjs/forms'
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { useMutation } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
diff --git a/__fixtures__/test-project/web/src/pages/DoublePage/DoublePage.tsx b/__fixtures__/test-project/web/src/pages/DoublePage/DoublePage.tsx
index fc0366665a3a..fafa953fb3a4 100644
--- a/__fixtures__/test-project/web/src/pages/DoublePage/DoublePage.tsx
+++ b/__fixtures__/test-project/web/src/pages/DoublePage/DoublePage.tsx
@@ -1,9 +1,9 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const DoublePage = () => {
return (
<>
-
+
DoublePage
diff --git a/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
index 8b383d5000ff..4d3f34fe28d3 100644
--- a/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
+++ b/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -39,7 +39,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
diff --git a/__fixtures__/test-project/web/src/pages/HomePage/HomePage.tsx b/__fixtures__/test-project/web/src/pages/HomePage/HomePage.tsx
index 4bf9f71d280b..290c7a31f29a 100644
--- a/__fixtures__/test-project/web/src/pages/HomePage/HomePage.tsx
+++ b/__fixtures__/test-project/web/src/pages/HomePage/HomePage.tsx
@@ -1,5 +1,5 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import BlogPostsCell from 'src/components/BlogPostsCell'
diff --git a/__fixtures__/test-project/web/src/pages/LoginPage/LoginPage.tsx b/__fixtures__/test-project/web/src/pages/LoginPage/LoginPage.tsx
index d6562c8e50cf..a61d5ffaedee 100644
--- a/__fixtures__/test-project/web/src/pages/LoginPage/LoginPage.tsx
+++ b/__fixtures__/test-project/web/src/pages/LoginPage/LoginPage.tsx
@@ -10,7 +10,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -46,7 +46,7 @@ const LoginPage = () => {
return (
<>
-
+
diff --git a/__fixtures__/test-project/web/src/pages/ProfilePage/ProfilePage.tsx b/__fixtures__/test-project/web/src/pages/ProfilePage/ProfilePage.tsx
index 39abea2fa43d..49911999021d 100644
--- a/__fixtures__/test-project/web/src/pages/ProfilePage/ProfilePage.tsx
+++ b/__fixtures__/test-project/web/src/pages/ProfilePage/ProfilePage.tsx
@@ -1,5 +1,5 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { useAuth } from 'src/auth'
@@ -12,7 +12,7 @@ const ProfilePage = () => {
return (
<>
-
+
Profile
diff --git a/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
index b5899cd3a218..191b39d43231 100644
--- a/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
+++ b/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
@@ -8,7 +8,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -59,7 +59,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
return (
<>
-
+
diff --git a/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx b/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx
index d1d0ac3a37f0..d92e41baeeb1 100644
--- a/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx
+++ b/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx
@@ -10,7 +10,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -49,7 +49,7 @@ const SignupPage = () => {
return (
<>
-
+
diff --git a/docs/docs/seo-head.md b/docs/docs/seo-head.md
index 4b50350c6cdd..58baabebb5b3 100644
--- a/docs/docs/seo-head.md
+++ b/docs/docs/seo-head.md
@@ -2,37 +2,40 @@
description: Use meta tags to set page info for SEO
---
-# SEO & Meta tags
+# SEO & `` tags
-## Add app title
-You certainly want to change the title of your Redwood app.
-You can start by adding or modify `title` inside `redwood.toml`
+Search Engine Optimization is a dark art that some folks dedicate their entire lives to. We've add a couple of features to Redwood to make HTML-based SEO fairly simple.
-```diff
+## Adding a Title
+
+You certainly want to change the title of your Redwood app from the default of "Redwood App." You can start by adding or modifing `title` inside of `/redwood.toml`
+
+```diff title=redwood.toml
[web]
- title = "Redwood App"
+ title = "My Cool App"
port = 8910
apiUrl = "/.redwood/functions"
```
+
This title (the app title) is used by default for all your pages if you don't define another one.
-It will also be use for the title template !
-### Title template
+It will also be use for the title template.
+
+### Title Template
+
Now that you have the app title set, you probably want some consistence with the page title, that's what the title template is for.
Add `titleTemplate` as a prop for `RedwoodProvider` to have a title template for every pages
-In _web/src/App.\{tsx,js\}_
-```diff
+```diff title=web/src/App.(tsx|jsx)
-
+
/* ... */
```
-You can write the format you like.
+You can use whatever formatting you'd like in here. Some examples:
-_Examples :_
```jsx
"%PageTitle | %AppTitle" => "Home Page | Redwood App"
@@ -41,18 +44,13 @@ _Examples :_
"%PageTitle : %AppTitle" => "Home Page : Redwood App"
```
-So now in your page you only need to write the title of the page.
+## Adding to Page ``
-## Adding to page ``
So you want to change the title of your page, or add elements to the `` of the page? We've got you!
+Let's say you want to change the title of your About page, Redwood provides a built in `` component, which you can use like this:
-Let's say you want to change the title of your About page,
-Redwood provides a built in `` component, which you can use like this
-
-
-In _AboutPage/AboutPage.\{tsx,js\}_
-```diff
+```diff title=web/src/pages/AboutPage/AboutPage.(tsx|jsx)
+import { Head } from '@redwoodjs/web'
const AboutPage = () => {
@@ -64,40 +62,50 @@ const AboutPage = () => {
+
```
-You can include any valid `` tag in here that you like, but just to make things easier we also have a utility component [MetaTags](#setting-meta-tags-open-graph-directives).
+You can include any valid `` tag in here that you like. However, Redwood also provides we also have a utility component [<Metadata>](#setting-meta-tags-and-opengraph-directives-with-metadata).
+
+:::caution `` Deprecation
+
+Prior to Redwood 7.0 this component was called `` and had several special hard-coded props like `ogContentUrl`, which didn't properly map to the OpenGraph spec. We'll still render `` for the foreseeable future, but it is deprecated and should migrate to `` if you have an existing app.
+
+:::
+
+### What About Nested Tags?
-### What about nested tags?
Redwood uses [react-helmet-async](https://github.com/staylor/react-helmet-async) underneath, which will use the tags furthest down your component tree.
For example, if you set title in your Layout, and a title in your Page, it'll render the one in Page - this way you can override the tags you wish, while sharing the tags defined in Layout.
+:::info Bots & `` Tags
+
+For these headers to appear to bots and scrapers e.g. for twitter to show your title, you have to make sure your page is prerendered. If your content is static you can use Redwood's built in [Prerender](prerender.md). For dynamic tags, check the [Dynamic head tags](#dynamic-tags)
-> **Side note**
-> for these headers to appear to bots and scrapers e.g. for twitter to show your title, you have to make sure your page is prerendered
-> If your content is static you can use Redwood's built in [Prerender](prerender.md). For dynamic tags, check the [Dynamic head tags](#dynamic-tags)
+:::
-## Setting meta tags / open graph directives
-Often we want to set more than just the title - most commonly to set "og" headers. Og standing for
-[open graph](https://ogp.me/) of course.
+## Setting `` Tags and OpenGraph Directives with ``
-Redwood provides a convenience component `` to help you get all the relevant tags with one go (but you can totally choose to do them yourself)
+Often we want to set more than just the title and description of the pageāmost commonly [OpenGraph](https://ogp.me/) headers.
+
+Redwood provides a convenience component `` to help you create most of these `` tags for you swith a more concise syntax. But, you can also pass children and define any custom content that you want.
+
+Here's an example setting some common meta, including a page title, description, `og:image` and an `http-equiv`:
-Here's an example setting some common headers, including how to set an `og:image`
```jsx
import { MetaTags } from '@redwoodjs/web'
const AboutPage = () => {
return (
-
AboutPage
-
+ og={{ image: "https://example.com/images/og.png", url: "https://example.com/start" }}
+ robots="nofollow"
+ >
+
+
+
+
About Page
This is the about page!
)
@@ -106,18 +114,213 @@ const AboutPage = () => {
export default AboutPage
```
-This is great not just for link unfurling on say Facebook or Slack, but also for SEO. Take a look at the [source](https://github.com/redwoodjs/redwood/blob/main/packages/web/src/components/MetaTags.tsx#L83) if you're curious what tags get set here.
+This code would be transformed into this HTML and injected into the `` tag:
+
+```html
+About page
+
+
+
+
+
+
+
+
+
+```
+
+Setting an `og:image` is how sites like Facebook and Slack can show a preview of a URL when pasted into a post (also known an "unfurling"):
+
+![Typical URL unfurl](/img/facebook_unfurl.png)
+
+Sites like GitHub go a step farther than a generic image by actually creating an image for a repo on the fly, including details about the repo itself:
+
+![GitHub's og:image for the redwood repo](https://opengraph.githubassets.com/322ce8081bb85a86397a59494eab1c0fbe942b5104461f625e2c973c46ae4179/redwoodjs/redwood)
+
+If you want to write your own `` tags, skipping the interpolation that `` does for you, you can pass them as children to `` or just write them into the `` tag as normal.
+
+### `` Props
+
+For the most part `` creates simple `` tags based on the structure of the props you pass in. There are a couple of special behaviors described below.
+
+#### Plain Key/Value Props
+
+Any "plain" key/value prop will be turned into a `` tag with `name` and `content` attributes:
+
+```jsx
+
+// generates
+
+```
+
+Child elements are just copied 1:1 to the resulting output:
+
+```jsx
+
+
+
+// generates
+
+
+```
+
+#### Passing Objects to Props
+
+Any props that contain an object will create a `` tag with `property` and `content` attributes, and the `property` being the names of the nested keys with a `:` between each:
+
+```jsx
+
+// generates
+
+```
+
+This is most commonly used to create the "nested" structure that a spec like OpenGraph uses:
+
+```jsx
+
+// generates
+
+```
+
+You can create multiple `` tags with the same name/property (allowed by the OpenGraph spec) by using an array:
+
+```jsx
+
+// generates
+
+
+```
+
+You can combine nested objects with strings to create any structure you like:
+
+```jsx
+
+// generates
+
+
+
+
+
+
+
+```
+
+#### Special OpenGraph Helpers
+
+If you define _any_ `og` prop, we will copy any `title` and `description` to an `og:title` and `og:description`:
+
+```jsx
+
+// generates
+
+
+```
+
+You can override this behavior by explictly setting `og:title` or `og:description` to `null`:
+
+```jsx
+
+// generates
+
+```
+
+Of course, if you don't want any auto-generated `og` tags, then don't include any `og` prop at all!
+
+In addition to `og:title` and `og:description`, if you define _any_ `og` prop we will generate an `og:type` set to `website`:
+
+```jsx
+
+// generates
+
+```
+
+You can override the `og:type` by setting it directly:
+
+```jsx
+
+// generates
+
+```
+
+#### Other Special Cases
+If you define a `title` prop we will automatically prepend a `` tag to the output:
+
+```jsx
+
+// generates
+My Website
+
+```
+
+If you define a `charSet` prop we will create a `` tag with the `charset` attribute:
+
+```jsx
+
+// generates
+
+```
+
+We simplifed some of the examples above by excluding the generated `` and `og:type` tags, so here's the real output if you included `title` and `og` props:
+
+```jsx
+
+// generates
+My Website
+
+
+
+
+```
+
+:::info Do I need to apply these same tags over and over in every page?
+
+Some `` tags, like `charset` or `locale` are probably applicable to the entire site, in which case it would be simpler to just include these once in your `index.html` instead of having to set them manually on each and every page/cell of your site.
+
+:::
+
+This should allow you to create a fairly full-featured set of `` tags with minimal special syntax! A typical `` invocation could look like:
+
+```jsx
+
+```
## Dynamic tags
-Great - so far we can see the changes, and bots will pick up our tags if we've prerendered the page, but what if I want to set the header based on the output of the Cell?
-> **Prerendering cells**
-> As of v3.x, Redwood supports prerendering your [Cells](https://redwoodjs.com/docs/cells) with the data you were querying. For more information please refer [to this section](https://redwoodjs.com/docs/prerender#cell-prerendering).
+Bots will pick up our tags if we've prerendered the page, but what if we want to set the `` based on the output of the Cell?
+:::info Prerendering
+
+As of v3.x, Redwood supports prerendering your [Cells](https://redwoodjs.com/docs/cells) with the data you were querying. For more information please refer [to this section](https://redwoodjs.com/docs/prerender#cell-prerendering).
+
+:::
+
+Let's say in our `PostCell`, we want to set the title to match the `Post`.
-Let's say in our PostCell, we want to set the title to match the Post.
```jsx
+import { Metadata } from '@redwoodjs/web'
+
import Post from 'src/components/Post/Post'
export const QUERY = gql`
@@ -139,7 +342,7 @@ export const Empty = /* ... */
export const Success = ({ post }) => {
return (
<>
- {
)
}
```
-Once the success component renders, it'll update your page's title and set the relevant meta tags for you!
+
+Once the `Success` component renders, it will update your page's `` and set the relevant `` tags for you!
diff --git a/docs/static/img/facebook_unfurl.png b/docs/static/img/facebook_unfurl.png
new file mode 100644
index 000000000000..780eca0b0697
Binary files /dev/null and b/docs/static/img/facebook_unfurl.png differ
diff --git a/packages/cli/src/commands/generate/__tests__/helpers.test.js b/packages/cli/src/commands/generate/__tests__/helpers.test.js
index 35bf51fb02cd..0e2b7179cd7a 100644
--- a/packages/cli/src/commands/generate/__tests__/helpers.test.js
+++ b/packages/cli/src/commands/generate/__tests__/helpers.test.js
@@ -9,12 +9,12 @@ import * as helpers from '../helpers'
import * as page from '../page/page'
const PAGE_TEMPLATE_OUTPUT = `import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const FooBarPage = () => {
return (
<>
-
+
FooBarPage
diff --git a/packages/cli/src/commands/generate/dbAuth/__tests__/__snapshots__/dbAuth.test.js.snap b/packages/cli/src/commands/generate/dbAuth/__tests__/__snapshots__/dbAuth.test.js.snap
index 2007b4e010c9..2229d11ab60c 100644
--- a/packages/cli/src/commands/generate/dbAuth/__tests__/__snapshots__/dbAuth.test.js.snap
+++ b/packages/cli/src/commands/generate/dbAuth/__tests__/__snapshots__/dbAuth.test.js.snap
@@ -13,7 +13,7 @@ exports[`dbAuth handler produces the correct files with custom password set via
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -48,7 +48,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -115,7 +115,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -151,7 +151,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -251,7 +251,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -302,7 +302,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -376,7 +376,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -414,7 +414,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -501,7 +501,7 @@ exports[`dbAuth handler produces the correct files with custom password set via
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -536,7 +536,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -603,7 +603,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -639,7 +639,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -739,7 +739,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -790,7 +790,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -864,7 +864,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -902,7 +902,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -989,7 +989,7 @@ exports[`dbAuth handler produces the correct files with custom username and pass
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1024,7 +1024,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -1091,7 +1091,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1127,7 +1127,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -1227,7 +1227,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1278,7 +1278,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -1352,7 +1352,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1390,7 +1390,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -1477,7 +1477,7 @@ exports[`dbAuth handler produces the correct files with custom username and pass
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1512,7 +1512,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -1579,7 +1579,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1615,7 +1615,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -1715,7 +1715,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1766,7 +1766,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -1840,7 +1840,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -1878,7 +1878,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -1965,7 +1965,7 @@ exports[`dbAuth handler produces the correct files with custom username and pass
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2000,7 +2000,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -2067,7 +2067,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2299,7 +2299,7 @@ const LoginPage = ({ type }) => {
return (
<>
-
+
@@ -2335,7 +2335,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2386,7 +2386,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -2460,7 +2460,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2498,7 +2498,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -2585,7 +2585,7 @@ exports[`dbAuth handler produces the correct files with custom username and pass
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2620,7 +2620,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -2687,7 +2687,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2723,7 +2723,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -2823,7 +2823,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2874,7 +2874,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -2948,7 +2948,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -2986,7 +2986,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -3073,7 +3073,7 @@ exports[`dbAuth handler produces the correct files with custom username set via
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3110,7 +3110,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -3179,7 +3179,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3215,7 +3215,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -3315,7 +3315,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3366,7 +3366,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -3442,7 +3442,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3480,7 +3480,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -3567,7 +3567,7 @@ exports[`dbAuth handler produces the correct files with custom username set via
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3604,7 +3604,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -3673,7 +3673,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3709,7 +3709,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -3809,7 +3809,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3860,7 +3860,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -3936,7 +3936,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -3974,7 +3974,7 @@ const SignupPage = () => {
return (
<>
-
+
@@ -4061,7 +4061,7 @@ exports[`dbAuth handler produces the correct files with default labels 1`] = `
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -4098,7 +4098,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
@@ -4167,7 +4167,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -4203,7 +4203,7 @@ const LoginPage = () => {
return (
<>
-
+
@@ -4303,7 +4303,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -4354,7 +4354,7 @@ const ResetPasswordPage = ({ resetToken }) => {
return (
<>
-
+
@@ -4430,7 +4430,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -4468,7 +4468,7 @@ const SignupPage = () => {
return (
<>
-
+
diff --git a/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template
index cf72f6efcb1e..06c34f0eaf64 100644
--- a/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template
+++ b/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -39,7 +39,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
diff --git a/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template
index b09688c201e5..88fac90acf42 100644
--- a/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template
+++ b/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template
@@ -10,7 +10,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -43,7 +43,7 @@ const LoginPage = () => {
return (
<>
-
+
diff --git a/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template
index eadd3d4222c9..bf25bef4cb64 100644
--- a/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template
+++ b/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template
@@ -10,7 +10,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -241,7 +241,7 @@ const LoginPage = ({ type }) => {
return (
<>
-
+
diff --git a/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template
index 232eaf8de0f2..bd84a9783d6b 100644
--- a/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template
+++ b/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template
@@ -8,7 +8,7 @@ import {
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -59,7 +59,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
return (
<>
-
+
diff --git a/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template
index e84e61ce0012..e3e1196f9b34 100644
--- a/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template
+++ b/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template
@@ -10,7 +10,7 @@ import {
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
@@ -45,7 +45,7 @@ const SignupPage = () => {
return (
<>
-
+
diff --git a/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap b/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap
index 9d727c16f3ea..7557712c9a30 100644
--- a/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap
+++ b/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap
@@ -2,12 +2,12 @@
exports[`Plural word files creates a page component with a plural word for name 1`] = `
"import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const CatsPage = () => {
return (
<>
-
+
CatsPage
@@ -27,12 +27,12 @@ export default CatsPage
exports[`Single world files creates a page component 1`] = `
"import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const HomePage = () => {
return (
<>
-
+
HomePage
@@ -83,7 +83,7 @@ describe('HomePage', () => {
exports[`TS Files TS Params 1`] = `
"import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
type TsParamFilesPageProps = {
id: string
@@ -92,7 +92,7 @@ type TsParamFilesPageProps = {
const TsParamFilesPage = ({ id }: TsParamFilesPageProps) => {
return (
<>
-
+
TsParamFilesPage
@@ -114,7 +114,7 @@ export default TsParamFilesPage
exports[`TS Files TS Params with type 1`] = `
"import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
type TsParamTypeFilesPageProps = {
id: number
@@ -123,7 +123,7 @@ type TsParamTypeFilesPageProps = {
const TsParamTypeFilesPage = ({ id }: TsParamTypeFilesPageProps) => {
return (
<>
-
+
TsParamTypeFilesPage
@@ -151,12 +151,12 @@ export default TsParamTypeFilesPage
exports[`TS Files generates typescript pages 1`] = `
"import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const TsFilesPage = () => {
return (
<>
-
+
TsFilesPage
@@ -249,12 +249,12 @@ describe('HomePage', () => {
exports[`handler file generation 3`] = `
{
"fileContent": "import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const HomePage = () => {
return (
<>
-
+
HomePage
@@ -333,12 +333,12 @@ describe('PostPage', () => {
exports[`handler file generation with route params 3`] = `
{
"fileContent": "import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const PostPage = ({ id }) => {
return (
<>
-
+
PostPage
@@ -380,12 +380,12 @@ export default Routes",
exports[`multiWorldFiles creates a page component 1`] = `
"import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const ContactUsPage = () => {
return (
<>
-
+
ContactUsPage
@@ -436,12 +436,12 @@ describe('ContactUsPage', () => {
exports[`paramFiles creates a page component with params 1`] = `
"import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const PostPage = ({ id }) => {
return (
<>
-
+
PostPage
diff --git a/packages/cli/src/commands/generate/page/page.js b/packages/cli/src/commands/generate/page/page.js
index f18fdbcae397..b20425d7e5b6 100644
--- a/packages/cli/src/commands/generate/page/page.js
+++ b/packages/cli/src/commands/generate/page/page.js
@@ -264,8 +264,8 @@ export const handler = async ({
task: (ctx, task) => {
task.title =
`One more thing...\n\n` +
- ` ${c.warning('Page created! A note about :')}\n\n` +
- ` At the top of your newly created page is a component,\n` +
+ ` ${c.warning('Page created! A note about :')}\n\n` +
+ ` At the top of your newly created page is a component,\n` +
` which contains the title and description for your page, essential\n` +
` to good SEO. Check out this page for best practices: \n\n` +
` https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets\n`
diff --git a/packages/cli/src/commands/generate/page/templates/page.tsx.template b/packages/cli/src/commands/generate/page/templates/page.tsx.template
index fc16315a0a96..046843373649 100644
--- a/packages/cli/src/commands/generate/page/templates/page.tsx.template
+++ b/packages/cli/src/commands/generate/page/templates/page.tsx.template
@@ -1,5 +1,5 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
<% if (paramName !== '') { %>
type ${pascalName}PageProps = {
${paramName}: ${paramType}
@@ -8,7 +8,7 @@ type ${pascalName}PageProps = {
const ${pascalName}Page = (<%- paramName === '' ? '' : `${propParam}: ${pascalName}PageProps` %>) => {
return (
<>
-
+
${pascalName}Page
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
index 2e0a2475db25..118a3c98c9c8 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { FieldError, Form, Label, Submit, TextField } from '@redwoodjs/forms'
@@ -38,7 +38,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js
index d63d1b4eebaf..1feebfb2a282 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js
@@ -1,4 +1,4 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import Hero from 'src/components/Hero/Hero'
import { useTranslation, Trans } from 'react-i18next'
import Highlight from 'react-highlight'
@@ -8,7 +8,7 @@ const HomePage = () => {
return (
<>
-
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx
index 928905af16f6..85b781ddf817 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx
@@ -1,11 +1,11 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobProfilesCell from 'src/components/Jobs/JobProfilesCell'
const AllJobProfilesPage = () => {
return (
<>
-
+
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx
index a584b40d7e6e..3ffce2b712ce 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx
@@ -1,11 +1,11 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobsCell from 'src/components/Jobs/JobsCell'
const AllJobsPage = () => {
return (
<>
-
+
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx
index 874baaabac7f..8c9ae6fe18bc 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx
@@ -1,10 +1,10 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobCell from 'src/components/Jobs/JobCell'
const JobPage = ({ id }) => {
return (
<>
-
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx
index 0594cde40f5c..10986e177626 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx
@@ -1,10 +1,10 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobProfileCell from 'src/components/Jobs/JobProfileCell'
const JobProfilePage = ({ id }) => {
return (
<>
-
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx
index 3ddde01ef36d..25574d6521e5 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx
@@ -1,12 +1,12 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobsCell from 'src/components/Jobs/JobsCell'
import JobProfilesCell from 'src/components/Jobs/JobProfilesCell'
const JobsPage = () => {
return (
<>
-
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx
index c55f55c520cc..a6d26888f38d 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx
@@ -1,4 +1,4 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const CREATE_JOB = gql`
@@ -12,7 +12,7 @@ const CREATE_JOB = gql`
const NewJobPage = ({ token }) => {
return (
<>
-
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx
index 4d9b60e36482..b0add733e726 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx
@@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags, useMutation } from '@redwoodjs/web'
+import { Metadata, useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import JobProfileForm from 'src/components/Jobs/JobProfileForm'
@@ -38,7 +38,7 @@ const NewJobProfilePage = () => {
return (
<>
-
diff --git a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js
index 80202a02cb7f..e729b4e00fab 100644
--- a/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js
+++ b/packages/internal/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js
@@ -9,7 +9,7 @@ import {
TextField,
} from '@redwoodjs/forms'
import { useAuth } from '@redwoodjs/auth'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
const LoginPage = () => {
@@ -40,7 +40,7 @@ const LoginPage = () => {
return (
<>
-
+
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
index 2e0a2475db25..118a3c98c9c8 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { FieldError, Form, Label, Submit, TextField } from '@redwoodjs/forms'
@@ -38,7 +38,7 @@ const ForgotPasswordPage = () => {
return (
<>
-
+
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js
index d63d1b4eebaf..1feebfb2a282 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/HomePage/HomePage.js
@@ -1,4 +1,4 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import Hero from 'src/components/Hero/Hero'
import { useTranslation, Trans } from 'react-i18next'
import Highlight from 'react-highlight'
@@ -8,7 +8,7 @@ const HomePage = () => {
return (
<>
-
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx
index 928905af16f6..85b781ddf817 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobProfilesPage/AllJobProfilesPage.tsx
@@ -1,11 +1,11 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobProfilesCell from 'src/components/Jobs/JobProfilesCell'
const AllJobProfilesPage = () => {
return (
<>
-
+
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx
index a584b40d7e6e..3ffce2b712ce 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/AllJobsPage/AllJobsPage.tsx
@@ -1,11 +1,11 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobsCell from 'src/components/Jobs/JobsCell'
const AllJobsPage = () => {
return (
<>
-
+
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx
index 874baaabac7f..8c9ae6fe18bc 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobPage/JobPage.tsx
@@ -1,10 +1,10 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobCell from 'src/components/Jobs/JobCell'
const JobPage = ({ id }) => {
return (
<>
-
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx
index 0594cde40f5c..10986e177626 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobProfilePage/JobProfilePage.tsx
@@ -1,10 +1,10 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobProfileCell from 'src/components/Jobs/JobProfileCell'
const JobProfilePage = ({ id }) => {
return (
<>
-
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx
index 3ddde01ef36d..25574d6521e5 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/JobsPage/JobsPage.tsx
@@ -1,12 +1,12 @@
import { Link, routes } from '@redwoodjs/router'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import JobsCell from 'src/components/Jobs/JobsCell'
import JobProfilesCell from 'src/components/Jobs/JobProfilesCell'
const JobsPage = () => {
return (
<>
-
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx
index c55f55c520cc..a6d26888f38d 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobPage/NewJobPage.tsx
@@ -1,4 +1,4 @@
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
const CREATE_JOB = gql`
@@ -12,7 +12,7 @@ const CREATE_JOB = gql`
const NewJobPage = ({ token }) => {
return (
<>
-
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx
index 4d9b60e36482..b0add733e726 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/Jobs/NewJobProfilePage/NewJobProfilePage.tsx
@@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { navigate, routes } from '@redwoodjs/router'
-import { MetaTags, useMutation } from '@redwoodjs/web'
+import { Metadata, useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import JobProfileForm from 'src/components/Jobs/JobProfileForm'
@@ -38,7 +38,7 @@ const NewJobProfilePage = () => {
return (
<>
-
diff --git a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js
index 80202a02cb7f..e729b4e00fab 100644
--- a/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js
+++ b/packages/vite/src/__tests__/fixtures/nestedPages/web/src/pages/LoginPage/LoginPage.js
@@ -9,7 +9,7 @@ import {
TextField,
} from '@redwoodjs/forms'
import { useAuth } from '@redwoodjs/auth'
-import { MetaTags } from '@redwoodjs/web'
+import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
const LoginPage = () => {
@@ -40,7 +40,7 @@ const LoginPage = () => {
return (
<>
-
+
diff --git a/packages/web/src/components/MetaTags.tsx b/packages/web/src/components/MetaTags.tsx
index 0f207a0f4dac..909c38e0a922 100644
--- a/packages/web/src/components/MetaTags.tsx
+++ b/packages/web/src/components/MetaTags.tsx
@@ -61,6 +61,7 @@ interface MetaTagsProps {
* using the open graph protocol https://ogp.me/
* @example
*
+ * @deprecated Please use instead
*/
export const MetaTags = (props: MetaTagsProps) => {
const {
diff --git a/packages/web/src/components/Metadata.test.tsx b/packages/web/src/components/Metadata.test.tsx
new file mode 100644
index 000000000000..9e3174fa8da8
--- /dev/null
+++ b/packages/web/src/components/Metadata.test.tsx
@@ -0,0 +1,471 @@
+import { render } from '@testing-library/react'
+
+import '@testing-library/jest-dom'
+import { Metadata } from './Metadata'
+
+// DOCS: can return a structured object from the database and just give it to `og` and it works
+
+describe('Metadata', () => {
+ beforeAll(() => {
+ // TODO: remove this once SSR is released
+ // this is just a workaround so we force the usage of PortalHead instead of Helmet for testing
+ globalThis.RWJS_ENV = {
+ RWJS_EXP_STREAMING_SSR: true,
+ }
+ })
+
+ it('renders nothing if no props or children', () => {
+ const input =
+ const output = <>>
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toEqual(render(output, { container: document.head }).container.innerHTML)
+ })
+
+ it('renders non-namespaced props', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders children', () => {
+ const input = (
+
+
+
+ )
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders props and children', () => {
+ const input = (
+
+
+
+ )
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders first-level namespaced props', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders multiple first-level namespaced props', () => {
+ const input = (
+
+ )
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders second-level namespaced props', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders combined first-level and second-level namespaced props', () => {
+ const input = (
+
+ )
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders an array of non-namespaced props', () => {
+ const input =
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders an array of namespaced props', () => {
+ const input = (
+
+ )
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders a mixture of namespaced array strings and objects', () => {
+ const input = (
+
+ )
+ const output = (
+ <>
+
+
+
+
+
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('adds a tag if `title` attribute present', () => {
+ const input =
+ const output = (
+ <>
+ My Title
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('adds multiple tags in proper order if `title` attribute is an array', () => {
+ const input =
+ const output = (
+ <>
+ Title 1
+ Title 2
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('adds an `og:title` tag if any `og` key present', () => {
+ const input =
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('does not automatically add `og:title` if already present', () => {
+ const input =
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('does not automatically add `og:title` if set to null', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).not.toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('adds an `og:description` tag if any `og` key present', () => {
+ const input =
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('does not automatically add `og:description` if already present', () => {
+ const input = (
+
+ )
+ const output = (
+ <>
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('does not automatically add `og:description` if set to null', () => {
+ const input = (
+
+ )
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).not.toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('adds an `og:type` tag if any `og` key present', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('does not automatically add `og:type` if already present', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('does not automatically add `og:type` if set to null', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).not.toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('does not create a standard name/content tag for the `charSet` prop', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).not.toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('adds a special `charSet` meta tag if `charSet` prop present', () => {
+ const input =
+ const output = (
+ <>
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toContain(
+ render(output, { container: document.head }).container.innerHTML
+ )
+ })
+
+ it('renders a typical collection of tags', () => {
+ const input = (
+
+ )
+ const output = (
+ <>
+ My Title
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+
+ expect(
+ render(input, { container: document.head }).container.innerHTML
+ ).toEqual(render(output, { container: document.head }).container.innerHTML)
+ })
+})
diff --git a/packages/web/src/components/Metadata.tsx b/packages/web/src/components/Metadata.tsx
new file mode 100644
index 000000000000..7b555df7d58d
--- /dev/null
+++ b/packages/web/src/components/Metadata.tsx
@@ -0,0 +1,111 @@
+import React from 'react'
+
+import { Head as HelmetHead } from '../index'
+
+// Ideally we wouldn't include this for non experiment builds
+// But.... not worth the effort to remove it from bundle atm
+import PortalHead from './PortalHead'
+
+type ValueOrCollection = T | ValueOrCollection[] | Record
+type ParentValue = ValueOrCollection
+
+const EXCLUDE_PROPS = ['charSet']
+
+const propToMetaTag = (
+ parentKey: string,
+ parentValue: ParentValue,
+ options: { attr: 'name' | 'property' }
+): JSX.Element | JSX.Element[] => {
+ if (Array.isArray(parentValue)) {
+ // array of attributes
+ return parentValue.flatMap((value) => {
+ return propToMetaTag(parentKey, value, options)
+ })
+ } else if (typeof parentValue === 'object') {
+ // namespaced attributes, name attribute changes to 'property'
+ return Object.entries(parentValue)
+ .filter(([_, v]) => v !== null)
+ .flatMap(([key, value]) => {
+ return propToMetaTag(`${parentKey}:${key}`, value, { attr: 'property' })
+ })
+ } else {
+ // plain text
+ const attributes = {
+ [options['attr']]: parentKey,
+ content: parentValue as string,
+ }
+ return
+ }
+}
+
+/**
+ * Add commonly used tags for unfurling/seo purposes
+ * using the open graph protocol https://ogp.me/
+ * @example
+ *
+ */
+export const Metadata = (props: Record) => {
+ const { children, ...metaProps } = props
+
+ let Head: typeof HelmetHead | typeof PortalHead = HelmetHead
+
+ if (RWJS_ENV.RWJS_EXP_STREAMING_SSR) {
+ Head = PortalHead
+ }
+
+ const tags: JSX.Element[] = Object.entries(metaProps)
+ .filter(
+ ([key, value]) =>
+ !EXCLUDE_PROPS.includes(key) &&
+ value !== null &&
+ (key !== 'og' || value !== true)
+ )
+ .flatMap(([key, value]) => {
+ return propToMetaTag(key, value, { attr: 'name' })
+ })
+ .filter((tag) => !!tag)
+
+ // custom overrides
+ if (metaProps.title) {
+ ;[metaProps.title]
+ .flat()
+ .reverse()
+ .map((title) => {
+ tags.unshift({title})
+ })
+ }
+
+ if (metaProps.charSet) {
+ tags.push()
+ }
+
+ if (metaProps.og) {
+ // add title and og:title
+ if (metaProps.title && !metaProps.og.title && metaProps.og.title !== null) {
+ tags.push()
+ }
+
+ // add og:description
+ if (
+ metaProps.description &&
+ !metaProps.og.description &&
+ metaProps.og.description !== null
+ ) {
+ tags.push(
+
+ )
+ }
+
+ // add og:type
+ if (!metaProps.og.type && metaProps.og.type !== null) {
+ tags.push()
+ }
+ }
+
+ return (
+
+ {tags.map((tag, i) => React.cloneElement(tag, { key: i }))}
+ {children}
+
+ )
+}
diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts
index 7774301aab5b..4a705e24cd35 100644
--- a/packages/web/src/index.ts
+++ b/packages/web/src/index.ts
@@ -30,4 +30,5 @@ export * from './graphql'
export * from './components/RedwoodProvider'
export * from './components/MetaTags'
+export * from './components/Metadata'
export { Helmet as Head, Helmet } from 'react-helmet-async'
diff --git a/tasks/test-project/codemods/blogPostPage.js b/tasks/test-project/codemods/blogPostPage.js
index 80bb9cb5ba45..6fd337b6ad71 100644
--- a/tasks/test-project/codemods/blogPostPage.js
+++ b/tasks/test-project/codemods/blogPostPage.js
@@ -2,7 +2,7 @@ const body = `
{
return (
<>
-
+
>
diff --git a/tasks/test-project/codemods/profilePage.js b/tasks/test-project/codemods/profilePage.js
index 3da4567fa0ca..21e3b12977ca 100644
--- a/tasks/test-project/codemods/profilePage.js
+++ b/tasks/test-project/codemods/profilePage.js
@@ -6,7 +6,7 @@ if (loading) {
return (
<>
-
+
Profile
diff --git a/tasks/test-project/tasks.js b/tasks/test-project/tasks.js
index 30784ea394fe..60cfbf24f83b 100644
--- a/tasks/test-project/tasks.js
+++ b/tasks/test-project/tasks.js
@@ -503,12 +503,12 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) {
const createPage = createBuilder('yarn redwood g page')
await createPage('double')
- const doublePageContent = `import { MetaTags } from '@redwoodjs/web'
+ const doublePageContent = `import { Metadata } from '@redwoodjs/web'
const DoublePage = () => {
return (
<>
-
+
DoublePage
diff --git a/tasks/test-project/tui-tasks.js b/tasks/test-project/tui-tasks.js
index 8c4fedb64656..2001faf64383 100644
--- a/tasks/test-project/tui-tasks.js
+++ b/tasks/test-project/tui-tasks.js
@@ -547,12 +547,12 @@ async function apiTasks(outputPath, { linkWithLatestFwBuild }) {
const createPage = createBuilder('yarn redwood g page')
await createPage('double')
- const doublePageContent = `import { MetaTags } from '@redwoodjs/web'
+ const doublePageContent = `import { Metadata } from '@redwoodjs/web'
const DoublePage = () => {
return (
<>
-
+
DoublePage