Skip to content

Commit

Permalink
Merge branch 'dev' into brophdawg11/sanitize-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Mar 15, 2023
2 parents c94ecce + 2072f33 commit a40c141
Show file tree
Hide file tree
Showing 22 changed files with 378 additions and 136 deletions.
38 changes: 36 additions & 2 deletions .changeset/meta-v2-enhancements.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
---
"@remix-run/cloudflare": minor
"@remix-run/deno": minor
"@remix-run/node": minor
"@remix-run/react": minor
"@remix-run/server-runtime": patch
"@remix-run/server-runtime": minor
---

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
We have made a few changes to the API for route module `meta` functions when using the `future.v2_meta` flag. **These changes are _only_ breaking for users who have opted in.**

- `V2_HtmlMetaDescriptor` has been renamed to `V2_MetaDescriptor`
- The `meta` function's arguments have been simplified
- `parentsData` has been removed, as each route's loader data is available on the `data` property of its respective `match` object
```tsx
// before
export function meta({ parentsData }) {
return [{ title: parentsData["routes/some-route"].title }];
}
// after
export function meta({ matches }) {
return [
{
title: matches.find((match) => match.id === "routes/some-route").data
.title,
},
];
}
```
- The `route` property on route matches has been removed, as relevant match data is attached directly to the match object
```tsx
// before
export function meta({ parentsData }) {
let rootModule = matches.find((match) => match.route.id === "root");
}
// after
export function meta({ matches }) {
let rootModule = matches.find((match) => match.id === "root");
}
```
- Added 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
5 changes: 5 additions & 0 deletions .changeset/proud-nails-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": minor
---

show deprecation warning when `browserBuildDirectory` config is used
2 changes: 2 additions & 0 deletions .changeset/vanilla-extract-cache.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
"@remix-run/dev": minor
"@remix-run/react": minor
"@remix-run/server-runtime": minor
---

Add experimental support for Vanilla Extract caching which can be enabled by setting `future.unstable_vanillaExtract: { cache: true }` in `remix.config`. This is considered experimental due to the use of a brand new Vanilla Extract compiler under the hood. Note that in order to use this feature, you must be using at least `v1.10.0` of `@vanilla-extract/css`.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
if: github.repository == 'remix-run/remix'
uses: ./.github/workflows/reusable-test.yml
with:
node_version: '["latest"]'
node_version: '["19.7"]'
15 changes: 13 additions & 2 deletions docs/file-conventions/remix-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ to `"build/index.js"`.

## serverBuildTarget

<docs-warning>This option is deprecated and will likely be removed in a future
stable release. Use a combination of [`publicPath`][public-path],
<docs-warning>This option is deprecated and will be removed in the next major version release. Use a combination of [`publicPath`][public-path],
[`serverBuildPath`][server-build-path], [`serverConditions`][server-conditions],
[`serverDependenciesToBundle`][server-dependencies-to-bundle]
[`serverMainFields`][server-main-fields], [`serverMinify`][server-minify],
Expand All @@ -136,6 +135,18 @@ The `serverBuildTarget` can be one of the following:
- [`"node-cjs"`][node-cjs]
- [`"vercel"`][vercel]

**Migration Table:**

| serverBuildTarget | publicPath | serverBuildPath | serverConditions | serverMainFields | serverModuleFormat | serverPlatform | serverDependenciesToBundle | serverMinify |
| -------------------- | ------------------ | --------------------------------------- | ---------------- | ----------------------- | ------------------ | -------------- | -------------------------- | ------------ |
| `arc` | `/\_static/build/` | `server/index.js` | | `main, module` | `cjs` | `node` | | `false` |
| `cloudflare-pages` | `/build/` | `functions/[[path]].js` | `worker` | `browser, module, main` | `esm` | `neutral` | `all` | `true` |
| `cloudflare-workers` | `/build/` | `build/index.js` | `worker` | `browser, module, main` | `esm` | `neutral` | `all` | `true` |
| `deno` | `/build/` | `build/index.js` | `deno, worker` | `module, main` | `esm` | `neutral` | `all` | `false` |
| `netlify` | `/build/` | `.netlify/functions-internal/server.js` | | `main, module` | `cjs` | `node` | | `false` |
| `node-cjs` | `/build/` | `build/index.js` | | `main, module` | `cjs` | `node` | | `false` |
| `vercel` | `/build/` | `api/index.js` | | `main, module` | `cjs` | `node` | | `false` |

## serverConditions

The order of conditions to use when resolving server dependencies' `exports`
Expand Down
149 changes: 99 additions & 50 deletions docs/route/meta.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ title: meta

# `meta`

The meta export defines object representations of `<meta>` tags for a route. These tags are important for SEO, browser behavior, and more.
The `meta` export allows you to add metadata HTML tags for every route in your app. These tags are important for things like search engine optimization (SEO) and browser directives for determining certain behaviors. They can also be used by social media sites to display rich previews of your app.

The meta export will set meta tags for your html document. We highly recommend setting the title and description on every route besides layout routes (their index route will set the meta).
The meta export will set meta tags for your html document. We highly recommend setting the title and description on every route aside from layout routes, as a layout's index route will set the meta for the index path.

```tsx
import type { MetaFunction } from "@remix-run/node"; // or cloudflare/deno
Expand Down Expand Up @@ -132,16 +132,14 @@ module.exports = {
};
```

The meta export allows you to add `<meta>` tags for every route in your app, including nested routes. These tags are important for SEO, browser behavior, and more.
The `meta` export allows you to add metadata HTML tags for every route in your app. These tags are important for things like search engine optimization (SEO) and browser directives for determining certain behaviors. They can also be used by social media sites to display rich previews of your app.

```tsx
import type { V2_MetaFunction } from "@remix-run/node"; // or cloudflare/deno

export const meta: V2_MetaFunction = () => {
return [
{
title: "New Remix App",
},
{ title: "New Remix App" },
{
name: "description",
content: "This app is a wildly dynamic web app",
Expand All @@ -150,55 +148,90 @@ export const meta: V2_MetaFunction = () => {
};
```

Meta functions return an array of `V2_HtmlMetaDescriptor` objects. These objects map one-to-one with normal HTML meta tags:
The `meta` function should return an array of `V2_MetaDescriptor` objects. These objects map one-to-one with HTML tags. So this meta function:

```tsx
const description = {
name: "description",
content: "This is my website description",
export const meta: V2_MetaFunction = () => {
return [
{ title: "Very cool app | Remix" },
{
property: "og:title",
content: "Very cool app",
},
{
name: "description",
content: "This app is the best",
},
];
};
// becomes
<meta
name="description"
content="This is my website description"
/>;

const ogTitle = {
property: "og:title",
content: "My Website Title",
```

…produces this HTML:

```html
<title>Very cool app | Remix</title>
<meta property="og:title" content="Very cool app" />;
<meta name="description" content="This app is the best" />
```

By default, meta descriptors will render a `<meta>` tag in most cases. The two exceptions are:

- `{ title }` renders a `<title>` tag
- `{ "script:ld+json" }` renders a `<script type="application/ld+json">` tag, and its value should be a serializable object that is stringified and injected into the tag.

```tsx
export const meta: V2_MetaFunction = () => {
return [
{
"script:ld+json": {
"@context": "https://schema.org",
"@type": "Organization",
name: "Remix",
url: "https://remix.run",
},
},
];
};
// becomes
<meta property="og:title" content="My Website Title" />;
```

The one exception is the `title` tag since it's not a `<meta>` tag but acts as one.
A meta descriptor can also render a `<link>` tag by setting the `tagName` property to `"link"`. This is useful for `<link>` tags associated with SEO like `canonical` URLs. For asset links like stylesheets and favicons, you should use the [`links` export][links-export] instead.

```tsx
const title = {
title: "My highly dynamic web *APP* with deep sessions",
export const meta: V2_MetaFunction = () => {
return [
{
tagName: "link",
rel: "canonical",
href: "https://remix.run",
},
];
};
// becomes
<title>
My highly dynamic web *APP* with deep sessions
</title>;
```

## `matches`
## `meta` Function Parameters

This is a list of the current route matches. You have access to many things, particularly the meta from the parent matches and data.
### `location`

It's most useful for merging the parent meta into the child meta since the child meta value is what will be used:
This is the current router `Location` object. This is useful for generating tags for routes at specific paths or query parameters.

```tsx
export const meta: V2_MetaFunction = ({ matches }) => {
let parentMeta = matches.map((match) => match.meta ?? []);
return [...parentMeta, { title: "Projects" }];
export const meta: V2_MetaFunction = ({ location }) => {
let searchQuery = new URLSearchParams(
location.search
).get("q");
return [{ title: `Search results for "${searchQuery}"` }];
};
```

## `data`
### `matches`

This is an array of the current route matches. You have access to many things, particularly the meta from the parent matches and data.

This is the data from your loader.
The interface for `matches` is similar to the return value of [`useMatches`][use-matches], but each match will include the output of its `meta` function. This is useful for [merging metadata across the route hierarchy][merging-metadata-across-the-route-hierarchy].

### `data`

This is the data from your route's loader.

```tsx
export async function loader({ params }: LoaderArgs) {
Expand All @@ -214,9 +247,13 @@ export const meta: V2_MetaFunction<typeof loader> = ({
};
```

## `parentsData`
### `params`

The route's URL params. See [Dynamic Segments in the Routing Guide][url-params].

Often you'll need the data from a parent route, you can look it up by route ID on `parentsData`.
## Accessing Data from Parent Route Loaders

In addition to the current route's data, often you'll want to access data from a route higher up in the route hierarchy. You can look it up by its route ID in `matches`.

```tsx filename=routes/project/$pid/tasks/$tid.tsx
import type { loader as projectDetailsLoader } from "../../../$pid";
Expand All @@ -228,17 +265,15 @@ export async function loader({ params }: LoaderArgs) {
export const meta: V2_MetaFunction<
typeof loader,
{ "routes/project/$pid": typeof projectDetailsLoader }
> = ({ data, parentsData }) => {
let project = parentsData["routes/project/$pid"].project;
> = ({ data, matches }) => {
let project = matches.find(
(match) => match.id === "routes/project/$pid"
).project;
let task = data.task;
return [{ title: `${project.name}: ${task.name}` }];
};
```

## `params`

The route URL params. See [Dynamic Segments in the Routing Guide][url-params].

## Gotchas with `meta` and Nested Routes

Because multiple nested routes render at the same time, there is some merging that needs to happen to determine the meta tags that ultimately render. Remix gives you complete control over this merge because there is no obvious default.
Expand Down Expand Up @@ -277,7 +312,7 @@ export const meta: V2_MetaFunction<typeof loader> = ({

With this code, we will lose the `viewport` meta tag at `/projects` and `/projects/123` because only the last meta is used and the code doesn't merge with the parent.

### Global Meta
### Global `meta`

Nearly every app will have global meta like the `viewport` and `charSet`. We recommend using normal `<meta>` tags inside of the [root route][root-route] instead of the `meta` export so you simply don't have to deal with merging:

Expand Down Expand Up @@ -310,24 +345,35 @@ export default function Root() {
}
```

### Avoid Meta in Parent Routes
### Avoid `meta` in Parent Routes

You can also avoid the merge problem by simply not exporting meta that you want to override from parent routes. Instead of defining meta on the parent route, use the [index route][index-route]. This way you can avoid complex merge logic for things like the title. Otherwise you will need to find the parent title descriptor and replace it with the child's title. It's much easier to simply not need to override by using index routes.

### Merging with Parent Meta
### Merging with Parent `meta`

Usually you only need to add meta to what the parent has already defined. You can merge parent meta with the spread operator and the [`matches`][matches] arg:

```tsx
export const meta: V2_MetaFunction = ({ matches }) => {
let parentMeta = matches.map((match) => match.meta ?? []);
let parentMeta = matches.flatMap(
(match) => match.meta ?? []
);
return [...parentMeta, { title: "Projects" }];
};
```

Note that this _will not_ override something like `title`. This is only additive.
Note that this _will not_ override something like `title`. This is only additive. If the inherited route meta includes a `title` tag, you can override with `Array.prototype.filter`:

```tsx
export const meta: V2_MetaFunction = ({ matches }) => {
let parentMeta = matches
.flatMap((match) => match.meta ?? [])
.filter((meta) => !("title" in meta));
return [...parentMeta, { title: "Projects" }];
};
```

### Meta Merging helper
### `meta` Merging helper

If you can't avoid the merge problem with global meta or index routes, we've created a helper that you can put in your app that can override and append to parent meta easily.

Expand All @@ -343,3 +389,6 @@ If you can't avoid the merge problem with global meta or index routes, we've cre
[index-route]: ../guides/routing#index-routes
[merge-meta]: https://gist.github.com/ryanflorence/ec1849c6d690cfbffcb408ecd633e069
[url-params]: ../guides/routing#dynamic-segments
[use-matches]: ../hooks/use-matches
[merging-metadata-across-the-route-hierarchy]: #md-merging-with-parent-meta
[links-export]: ./links
47 changes: 47 additions & 0 deletions integration/flat-routes-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PlaywrightFixture } from "./helpers/playwright-fixture";
import type { Fixture, AppFixture } from "./helpers/create-fixture";
import { createFixtureProject } from "./helpers/create-fixture";
import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
import { flatRoutesWarning } from "../packages/remix-dev/config";

let fixture: Fixture;
let appFixture: AppFixture;
Expand Down Expand Up @@ -147,6 +148,52 @@ test.describe("flat routes", () => {
}
});

test.describe("warns when v1 routesConvention is used", () => {
let buildStdio = new PassThrough();
let buildOutput: string;

let originalConsoleLog = console.log;
let originalConsoleWarn = console.warn;
let originalConsoleError = console.error;

test.beforeAll(async () => {
console.log = () => {};
console.warn = () => {};
console.error = () => {};
await createFixtureProject({
buildStdio,
future: { v2_routeConvention: false },
files: {
"routes/index.tsx": js`
export default function () {
return <p>routes/index</p>;
}
`,
},
});

let chunks: Buffer[] = [];
buildOutput = await new Promise<string>((resolve, reject) => {
buildStdio.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
buildStdio.on("error", (err) => reject(err));
buildStdio.on("end", () =>
resolve(Buffer.concat(chunks).toString("utf8"))
);
});
});

test.afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
});

test("warns about conflicting routes", () => {
console.log(buildOutput);
expect(buildOutput).toContain(flatRoutesWarning);
});
});

test.describe("emits warnings for route conflicts", async () => {
let buildStdio = new PassThrough();
let buildOutput: string;
Expand Down
Loading

0 comments on commit a40c141

Please sign in to comment.