๐บ๏ธ v2 meta
API
#4462
Replies: 14 comments 28 replies
-
I may be missing something, but with the new API, how would a child route override the document title specified by a parent route? |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
Big fan of this proposal! I just have a few comments, likely nothing super actionable (at least at this stage)
I'm very happy about this! Personally, I remember finding it confusing that
Regarding the return type signature, I would definitely recommend adding more type checking and specificity to the union. I think many TS users rely a lot on autocomplete with types to avoid them having to go to the docs ๐
True, but another way to say this is "with this API we're making better Web Developers, not better Remix Developers". I know it's a small thing to learn, but I do worry for new developers who come to Remix that they'll miss out a bit on how meta tags actually work and what all the attributes are. Requiring proper attributes to be supplied means developers will likely spend more time in the MDN docs than the Remix docs, because they'll be learning how meta tags work holistically. This means they can now more easily take their knowledge with them if they're ever unfortunate enough to not work in Remix. Anyway, I know that's probably a small thing, just wanted to give a suggestion in terms of "messaging".
Big fan of exposing an API like this! |
Beta Was this translation helpful? Give feedback.
-
Makes sense why we'd want a lower-level API, cool to see! ๐ I can see the motivation, even though I have never had this problem myself yet. ๐ |
Beta Was this translation helpful? Give feedback.
-
I love this, especially the addition of the matches object!
I like the name, and it's probably convenient (especially for the migration from v1 to v2), but I am also okay with having a copy-paste version somewhere in the docs. What would be a great value-add for me would be some autocompletion in VS Code (aka. typing) for some of the more popular meta tag names and properties, but I understand if this is out of scope or even messy. |
Beta Was this translation helpful? Give feedback.
-
Would this be the right place to mention the TypeScript type definitions of The issue I'm having is that when I declare the type of the Here's a very simplistic example: export const meta: MetaFunction<typeof loader> = ({ data }) => {
// On a 404 page, `data` will be undefined, so this will throw a TypeError and
// render an error page instead of the intended CatchBoundary 404 page.
// The problem is that TypeScript thinks `data` will always be defined.
return {
title: data.name,
};
};
export const loader = ({ params }: LoaderArgs) => {
const blogPost = getBlogPost(params.id);
if (!blogPost) {
throw new Response('Not found', { status: 404 });
}
return json(blogPost);
};
export function CatchBoundary() {
return <h1>Blog Post Not Found</h1>;
}
function getBlogPost(id?: string) {
if (id === undefined || id === 'bad-blog-post-id') {
return undefined;
}
return { id, name: `Cool post ${id}` };
} I can work around this by discarding the type LoaderData = SerializeFrom<typeof loader>;
export const meta = ({ data }: { data: LoaderData | undefined }) => {
if (!data) {
return { title: 'Blog Post Not Found' };
}
return { title: data.name };
}; |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Is it intentional that every meta export in the entire routing tree is re-evaluated on every internal navigation? Doesn't seem very useful now that meta no longer inherits from parent by default. |
Beta Was this translation helpful? Give feedback.
-
I'm really exited about this new version! It's a great improvement. Sadly, I got a bit disappointed when actually testing this out because I misunderstood something from the RFC:
In the current implementation of this, the
This is export interface RouteMatch {
id: string;
pathname: string;
params: Params<string>;
data: RouteData;
handle: undefined | {
[key: string]: any;
};
} โฆand this is export interface RouteMatchWithMeta<Route> extends BaseRouteMatch<Route> {
meta: V2_HtmlMetaDescriptor[];
}
// BaseRouteMatch is an alias to:
export interface RouteMatch<Route> {
params: Params;
pathname: string;
route: Route;
} Is this intentional? Will it change later? I wanted to use this because I had a usecase where I could set the page title in one place in Something like this: export const meta: V2_MetaFunction = ({ data, matches }) => {
const appName = "Example App";
// Hande data being possibly undefined due to an error somewhere.
let title = data?.defaultPageTitle || appName;
// Look for page titles in the whole route hierarchy. The last/deepest title wins.
const titles = matches.map((match) => {
const matchTitle = match.data["$pageTitle"];
if (typeof matchTitle === "string" && matchTitle.length > 0) {
return matchTitle;
} else {
return null;
}
}).filter(Boolean);
if (titles.length > 0) {
title = titles[title.length - 1];
}
return [
{
charset: "utf-8",
viewport: "width=device-width,initial-scale=1",
title
},
];
}; Before in v1, I had to repeat this code in every single route that expose a export const meta: MetaFunction<typeof loader> = ({ data }) =>
setPageTitle(data); (using a helper method that is imported that does similar logic to the first code example) Title is set from the |
Beta Was this translation helpful? Give feedback.
-
Just wanted to drop in to say that I Iove the idea of merged meta fields being an explicit thing. This has tripped me up recently with |
Beta Was this translation helpful? Give feedback.
-
I took a stab at creating a mergeMeta helper function [pastes code and ducks]. I'm sure that anyone else in here that might be working on this will come up with something much better - but this got me most of the way there for now. And I completely ignore types - until the v1.10.0 release.
/**
* meta
* @returns V2_MetaFunction
* TODO: ts type for meta
* New v2 meta api
* https://github.com/remix-run/remix/releases/tag/remix%401.8.0
* https://github.com/remix-run/remix/discussions/4462
* V2_MetaFunction interface is currently in v1.10.0-pre.5
*/
export const meta = ({ data }: any) => {
return [
{ charset: 'utf-8' },
{ title: 'Remix Workbench App' },
{ name: 'description', content: 'A Remix demo app with CSS, Tailwind, Radix UI and other headless UI components.' },
{ name: 'viewport', content: 'width=device-width,initial-scale=1' },
{ name: 'theme-color', content: '#f59e0b' },
{ name: 'msapplication-TileColor', content: '#f59e0b' },
{ property: 'og:title', content: 'Remix Workbench App' },
{ property: 'og:description', content: 'A Remix demo app with CSS, Tailwind, Radix UI and other headless UI components.' },
// Note - og:url will not update on route changes, but it should be fine for
// og links being crawled or shared (i.e. a full SSR)
{ property: 'og:url', content: getUrl(data?.origin, data?.path) },
{ property: 'og:type', content: 'website' },
{ property: 'og:image', content: 'https://remix.infonomic.io/og.png' },
]
}
/**
* meta
* @returns V2_MetaFunction
* TODO: ts types for meta
* New v2 meta api
* https://github.com/remix-run/remix/releases/tag/remix%401.8.0
* https://github.com/remix-run/remix/discussions/4462
* V2_MetaFunction interface is currently in v1.10.0-pre.5
*/
export const meta = ({ matches }: any) => {
const title = 'Home - Remix Workbench App'
return mergeMeta(matches,
[
{ title },
{ property: 'og:title', content: title },
]
)
}
/**
* mergeMeta
* @returns [] merged metatags
* TODO: this is a temporary merge meta function for the
* new v2 meta api. It may not be complete or the best way
* to do this - but it works for the moment.
* TODO: types
* https://github.com/remix-run/remix/releases/tag/remix%401.8.0
* https://github.com/remix-run/remix/discussions/4462
* V2_MetaFunction interface is currently in v1.10.0-pre.5
*/
export function mergeMeta(matches: any, tags: any[] = []) {
function findMatch(upperTag: any, tag: any) {
let found = false
const rules = [
{ k: 'charSet', f: () => !!tag.charSet },
{ k: 'title', f: () => !!tag.title },
{ k: 'name', f: () => upperTag.name === tag.name },
{ k: 'property', f: () => upperTag.property === tag.property },
{ k: 'httpEquiv', f: () => upperTag.httpEquiv === tag.httpEquiv },
]
for (let index = 0; index < rules.length; index += 1) {
const rule = rules[index]
if (upperTag[rule.k] !== undefined) {
found = rule.f()
break
}
}
return found
}
const filteredMeta = matches.map((match: any) => match.meta)
.map((upperTags: any[]) => {
const filteredUpperTags: any[] = []
for (const upperTag of upperTags) {
let found = false
for (const tag of tags) {
found = findMatch(upperTag, tag)
if (found) break
}
if (!found) filteredUpperTags.push(upperTag)
}
return filteredUpperTags
})
return [...filteredMeta, tags]
} |
Beta Was this translation helpful? Give feedback.
-
Few suggestions:
|
Beta Was this translation helpful? Give feedback.
-
Here's an updated attempt at a /**
* A utility function for the v2 meta API. It will
* merge (filter) root metatags - replacing any that match
* the supplied route module meta tags. It may not be complete
* or the best way to do this but it works for the moment.
* https://github.com/remix-run/remix/releases/tag/remix%401.8.0
* https://github.com/remix-run/remix/discussions/4462
*
* @returns {V2_HtmlMetaDescriptor[]} Merged metatags
*/
export function mergeMeta(matches: any, tags: V2_HtmlMetaDescriptor[] = []): V2_HtmlMetaDescriptor[] {
const rootModule = matches.find((match: any) => match.route.id === 'root')
const rootMeta = rootModule.meta
function findMatch(rootTag: any, tag: any) {
const rules = [
{ k: 'charSet', f: () => !!tag.charSet },
{ k: 'title', f: () => !!tag.title },
{ k: 'name', f: () => rootTag.name === tag.name },
{ k: 'property', f: () => rootTag.property === tag.property },
{ k: 'httpEquiv', f: () => rootTag.httpEquiv === tag.httpEquiv },
]
for (const rule of rules) {
if (rootTag[rule.k] !== undefined) {
return rule.f()
}
}
return false
}
if (rootMeta) {
const filteredRootMeta = rootMeta
.filter((rootTag: V2_HtmlMetaDescriptor) => {
for (const tag of tags) {
if (findMatch(rootTag, tag)) {
return false
}
}
return true
})
return [...filteredRootMeta, tags]
} else {
return tags
}
} which can be used in a route module as... /**
* meta
* @returns {V2_MetaFunction}
*/
export const meta: V2_MetaFunction = ({ data, matches }): V2_HtmlMetaDescriptor[] => {
const title = `Note - ${truncate(data?.note?.title, 50, true)} - Remix Workbench App`
return mergeMeta(matches, [{ title }, { property: 'og:title', content: title }])
} Thoughts or suggestions welcome. |
Beta Was this translation helpful? Give feedback.
-
This might sound a bit weird at first, given Remix already has a
To build a canonical link, we absolutely need loader data. We can roll our own dynamic link API using |
Beta Was this translation helpful? Give feedback.
-
meta
API for Remix v2loader
data when it throwsย #3903actionData
available inmeta
functionย #3534MetaFunction
orLinkFunction
is not caught byErrorBoundary
ย #3140meta
exportย #2598Summary
This is a proposal for a new, more flexible API for route
meta
functions. Instead of returning an object with key/value pairings for a tag's name and content, the return signature formeta
would look more likelinks
, returning an array of objects expressing each tag's attribute values.Additionally, this proposal will change how Remix handles
meta
in nested routes. When nested routes export ameta
function, Remix will no longer automatically render meta tags from all matched routes in the tree. Routes will only rendermeta
exported directly from the match itself, falling back to the most recent match only ifmeta
is omitted from the leaf route.Motivation
The
meta
API as designed is nice for a few reasons:The return signature tracks well with how most devs normally think about meta tags.
<meta>
is essentially a key/value pair, so returning an object that maps to rendered tags seems intuitive.The key and value are generally provided via the
name
andcontent
attributes respectively. But some tags use non-standard attributes. Tags that follow the OpenGraph protocol generally useproperty
instead ofname
. These are hard to remember, so having Remix map them for you is a nice convenience.We support a
title
value that maps to a<title>
tag instead of<meta>
, as the page title is still metadata. This means that updating the page title is as simple as updating any other page meta.With nested routing, you get the benefit of inheriting meta tags from parent routes. This is really nice if you only need to set one or two tags on a leaf node without a lot of duplication in a deeply nested route tree.
While these features feel nice for most cases, Remix makes some assumptions about your app to implement them. These assumptions result in limitations with the API today:
Meta tags should only have two attributes that map to a key and a value. Additional attributes cannot be passed to meta tags.
We assume that our mapping of key/value pairs matches developer intent 100% of the time. It is impossible for you to tell us that you want to map to different attributes.
Keys later in the tree will override matching keys higher up in the tree, which as an object-assignment should be expected. But what if you want to append to a key instead of overriding it (for example: array values for
article:author
tags)?Meta tags in nested routes are always inherited, but what if you want a leaf route to opt out of rendering one or more meta tags from an ancestor route? We don't provide a way to do this today.
Opt-in
The proposed API constitutes a breaking change but will not be the default behavior in v1. The old API will continue to be the default behavior if developers take no action. Apps will opt-in to the new API with a feature flag in
remix.config.js
.Detailed design
The new
meta
function would look like this:Return type signature
The return type signature of
meta
today looks like this:The new type signature will look something like this (this is for explanation purposes at the moment and may need more complexity if we wish to provide any type checking for specific attributes like we do with links):
Drawbacks
It's a lower-level API
The nice thing about the existing API is that it's simple, and you don't have to remember the API for deviations from the standard
name
/content
attributes. This change would ask a bit more from developers, but we think the tradeoff is worth it given current limitations.The good news is that we can adapt our existing code to provide a pretty straight-forward helper function that would help. We'd probably let the app own this code rather than expose it directly. After all, the whole point of this change is to give decisions about meta tags back to you.
We could also expose a helper for folks who want to use arrays for more granular control, but handles naive merging just as we do today.
Out of scope
There are other issues with
meta
that will still need to be resolved but are not in scope of this work:<Meta />
#3140meta
.These issues will be addressed in later RFCs.
Beta Was this translation helpful? Give feedback.
All reactions