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

🗺 API updates for v2 meta arguments and types #5785

Merged
merged 8 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions .changeset/meta-v2-enhancements.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
---
"@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
27 changes: 16 additions & 11 deletions docs/route/meta.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ 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:
Meta functions return an array of `V2_MetaDescriptor` objects. These objects map one-to-one with normal HTML meta tags:
chaance marked this conversation as resolved.
Show resolved Hide resolved

```tsx
const description = {
Expand Down Expand Up @@ -187,14 +187,7 @@ const title = {

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

It's most useful for merging the parent meta into the child meta since the child meta value is what will be used:

```tsx
export const meta: V2_MetaFunction = ({ matches }) => {
let parentMeta = matches.map((match) => match.meta ?? []);
return [...parentMeta, { title: "Projects" }];
};
```
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](#md-merging-with-parent-meta).

## `data`

Expand Down Expand Up @@ -320,12 +313,23 @@ Usually you only need to add meta to what the parent has already defined. You ca

```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

Expand All @@ -343,3 +347,4 @@ 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
2 changes: 1 addition & 1 deletion integration/meta-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ test.describe("v2_meta", () => {

"app/routes/music.jsx": js`
export function meta({ data, matches }) {
let rootModule = matches.find(match => match.route.id === "root");
let rootModule = matches.find(match => match.id === "root");
let rootCharSet = rootModule.meta.find(meta => meta.charSet);
return [
rootCharSet,
Expand Down
7 changes: 5 additions & 2 deletions packages/remix-cloudflare/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export type {
HeadersFunction,
HtmlLinkDescriptor,
HtmlMetaDescriptor,
V2_HtmlMetaDescriptor,
JsonFunction,
LinkDescriptor,
LinksFunction,
Expand All @@ -66,14 +65,18 @@ export type {
MemoryUploadHandlerOptions,
MetaDescriptor,
MetaFunction,
V2_MetaFunction,
PageLinkDescriptor,
RequestHandler,
RouteComponent,
RouteHandle,
SerializeFrom,
ServerBuild,
ServerEntryModule,
V2_ServerRuntimeMetaArgs as V2_MetaArgs,
V2_ServerRuntimeMetaDescriptor as V2_MetaDescriptor,
// TODO: Remove in v2
V2_ServerRuntimeMetaDescriptor as V2_HtmlMetaDescriptor,
V2_ServerRuntimeMetaFunction as V2_MetaFunction,
Session,
SessionData,
SessionIdStorageStrategy,
Expand Down
7 changes: 5 additions & 2 deletions packages/remix-deno/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export type {
SerializeFrom,
ServerBuild,
ServerEntryModule,
V2_ServerRuntimeMetaArgs as V2_MetaArgs,
V2_ServerRuntimeMetaDescriptor as V2_MetaDescriptor,
// TODO: Remove in v2
V2_ServerRuntimeMetaDescriptor as V2_HtmlMetaDescriptor,
V2_ServerRuntimeMetaFunction as V2_MetaFunction,
Session,
SessionData,
SessionIdStorageStrategy,
Expand All @@ -70,6 +75,4 @@ export type {
UnsignFunction,
UploadHandler,
UploadHandlerPart,
V2_HtmlMetaDescriptor,
V2_MetaFunction,
} from "@remix-run/server-runtime";
7 changes: 5 additions & 2 deletions packages/remix-node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export type {
HeadersFunction,
HtmlLinkDescriptor,
HtmlMetaDescriptor,
V2_HtmlMetaDescriptor,
JsonFunction,
LinkDescriptor,
LinksFunction,
Expand All @@ -77,14 +76,18 @@ export type {
MemoryUploadHandlerOptions,
MetaDescriptor,
MetaFunction,
V2_MetaFunction,
PageLinkDescriptor,
RequestHandler,
RouteComponent,
RouteHandle,
SerializeFrom,
ServerBuild,
ServerEntryModule,
V2_ServerRuntimeMetaArgs as V2_MetaArgs,
V2_ServerRuntimeMetaDescriptor as V2_MetaDescriptor,
// TODO: Remove in v2
V2_ServerRuntimeMetaDescriptor as V2_HtmlMetaDescriptor,
V2_ServerRuntimeMetaFunction as V2_MetaFunction,
Session,
SessionData,
SessionIdStorageStrategy,
Expand Down
68 changes: 39 additions & 29 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ import {
import type { HtmlLinkDescriptor, PrefetchPageDescriptor } from "./links";
import { createHtml, escapeHtml } from "./markup";
import type {
RouteMatchWithMeta,
V1_HtmlMetaDescriptor,
V2_HtmlMetaDescriptor,
V1_MetaFunction,
V2_MetaDescriptor,
V2_MetaFunction,
V2_MetaMatch,
V2_MetaMatches,
} from "./routeModules";
import type {
Transition,
Expand Down Expand Up @@ -568,12 +571,11 @@ function V1Meta() {
if (routeModule.meta) {
let routeMeta =
typeof routeModule.meta === "function"
? routeModule.meta({
? (routeModule.meta as V1_MetaFunction)({
chaance marked this conversation as resolved.
Show resolved Hide resolved
data,
parentsData,
params,
location,
matches: undefined as any,
})
: routeModule.meta;
if (routeMeta && Array.isArray(routeMeta)) {
Expand Down Expand Up @@ -651,39 +653,28 @@ function V1Meta() {

function V2Meta() {
let { routeModules } = useRemixContext();
let { matches, loaderData } = useDataRouterStateContext();
let { matches: _matches, loaderData } = useDataRouterStateContext();
let location = useLocation();

let meta: V2_HtmlMetaDescriptor[] = [];
let leafMeta: V2_HtmlMetaDescriptor[] | null = null;
let parentsData: { [routeId: string]: AppData } = {};

let matchesWithMeta: RouteMatchWithMeta[] = matches.map((match) => ({
...match,
meta: [],
}));

let index = -1;
for (let match of matches) {
index++;
let routeId = match.route.id;
let meta: V2_MetaDescriptor[] = [];
let leafMeta: V2_MetaDescriptor[] | null = null;
let matches: V2_MetaMatches = [];
for (let i = 0; i < _matches.length; i++) {
let _match = _matches[i];
let routeId = _match.route.id;
let data = loaderData[routeId];
let params = match.params;

let params = _match.params;
let routeModule = routeModules[routeId];

let routeMeta: V2_HtmlMetaDescriptor[] | V1_HtmlMetaDescriptor | undefined =
[];
let routeMeta: V2_MetaDescriptor[] | V1_HtmlMetaDescriptor | undefined = [];

if (routeModule?.meta) {
routeMeta =
typeof routeModule.meta === "function"
? routeModule.meta({
? (routeModule.meta as V2_MetaFunction)({
chaance marked this conversation as resolved.
Show resolved Hide resolved
data,
parentsData,
params,
location,
matches: matchesWithMeta,
matches,
})
: routeModule.meta;
} else if (leafMeta) {
Expand All @@ -697,7 +688,7 @@ function V2Meta() {
if (!Array.isArray(routeMeta)) {
throw new Error(
"The `v2_meta` API is enabled in the Remix config, but the route at " +
match.route.path +
_match.route.path +
" returns an invalid value. In v2, all route meta functions must " +
"return an array of meta objects." +
// TODO: Add link to the docs once they are written
Expand All @@ -706,9 +697,28 @@ function V2Meta() {
);
}

matchesWithMeta[index].meta = routeMeta;
let match: V2_MetaMatch = {
id: routeId,
data,
meta: routeMeta,
params: _match.params,
pathname: _match.pathname,
handle: _match.route.handle,
// TODO: Remove in v2. Only leaving it for now because we used it in
// examples and there's no reason to crash someone's build for one line.
// They'll get a TS error from the type updates anyway.
// @ts-expect-error
get route() {
console.warn(
"The meta function in " +
_match.route.path +
" accesses the `route` property on `matches`. This is deprecated and will be removed in Remix version 2. See"
);
return _match.route;
},
};
matches[i] = match;
meta = routeMeta;
parentsData[routeId] = data;
leafMeta = meta;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/remix-react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export type { HtmlLinkDescriptor } from "./links";
export type {
CatchBoundaryComponent,
HtmlMetaDescriptor,
V2_MetaArgs,
V2_MetaDescriptor,
V2_MetaFunction,
RouteModules as UNSAFE_RouteModules,
ShouldReloadFunction,
} from "./routeModules";
Expand Down
53 changes: 42 additions & 11 deletions packages/remix-react/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Location,
ShouldRevalidateFunction,
} from "react-router-dom";
import type { LoaderFunction, SerializeFrom } from "@remix-run/server-runtime";

import type { AppData } from "./data";
import type { LinkDescriptor } from "./links";
Expand All @@ -25,7 +26,7 @@ export interface RouteModule {
| V1_MetaFunction
| V1_HtmlMetaDescriptor
| V2_MetaFunction
| V2_HtmlMetaDescriptor[];
| V2_MetaDescriptor[];
shouldRevalidate?: ShouldRevalidateFunction;
}

Expand Down Expand Up @@ -84,17 +85,47 @@ export interface V1_MetaFunction {
export type MetaFunction = V1_MetaFunction;

export interface RouteMatchWithMeta extends DataRouteMatch {
meta: V2_HtmlMetaDescriptor[];
meta: V2_MetaDescriptor[];
}

export interface V2_MetaFunction {
(args: {
data: AppData;
parentsData: RouteData;
params: Params;
location: Location;
matches: RouteMatchWithMeta[];
}): V2_HtmlMetaDescriptor[] | undefined;
export interface V2_MetaMatch<
RouteId extends string = string,
Loader extends LoaderFunction | unknown = unknown
> {
id: RouteId;
pathname: DataRouteMatch["pathname"];
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown;
handle?: unknown;
params: DataRouteMatch["params"];
meta: V2_MetaDescriptor[];
}

export type V2_MetaMatches<
MatchLoaders extends Record<string, unknown> = Record<string, unknown>
> = Array<
{
[K in keyof MatchLoaders]: V2_MetaMatch<
Exclude<K, number | symbol>,
MatchLoaders[K]
>;
}[keyof MatchLoaders]
>;

export interface V2_MetaArgs<
Loader extends LoaderFunction | unknown = unknown,
MatchLoaders extends Record<string, unknown> = Record<string, unknown>
> {
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : AppData;
params: Params;
location: Location;
matches: V2_MetaMatches<MatchLoaders>;
}

export interface V2_MetaFunction<
Loader extends LoaderFunction | unknown = unknown,
MatchLoaders extends Record<string, unknown> = Record<string, unknown>
> {
(args: V2_MetaArgs<Loader, MatchLoaders>): V2_MetaDescriptor[] | undefined;
}

/**
Expand All @@ -118,7 +149,7 @@ export interface V1_HtmlMetaDescriptor {
// TODO: Replace in v2
export type HtmlMetaDescriptor = V1_HtmlMetaDescriptor;

export type V2_HtmlMetaDescriptor =
export type V2_MetaDescriptor =
| { charSet: "utf-8" }
| { title: string }
| { name: string; content: string }
Expand Down
Loading