-
-
Save ryanflorence/ec1849c6d690cfbffcb408ecd633e069 to your computer and use it in GitHub Desktop.
import type { V2_HtmlMetaDescriptor, V2_MetaFunction } from "@remix-run/node"; | |
export const mergeMeta = ( | |
overrideFn: V2_MetaFunction, | |
appendFn?: V2_MetaFunction, | |
): V2_MetaFunction => { | |
return arg => { | |
// get meta from parent routes | |
let mergedMeta = arg.matches.reduce((acc, match) => { | |
return acc.concat(match.meta || []); | |
}, [] as V2_HtmlMetaDescriptor[]); | |
// replace any parent meta with the same name or property with the override | |
let overrides = overrideFn(arg); | |
for (let override of overrides) { | |
let index = mergedMeta.findIndex( | |
meta => | |
("name" in meta && | |
"name" in override && | |
meta.name === override.name) || | |
("property" in meta && | |
"property" in override && | |
meta.property === override.property) || | |
("title" in meta && "title" in override), | |
); | |
if (index !== -1) { | |
mergedMeta.splice(index, 1, override); | |
} | |
} | |
// append any additional meta | |
if (appendFn) { | |
mergedMeta = mergedMeta.concat(appendFn(arg)); | |
} | |
return mergedMeta; | |
}; | |
}; |
import { mergeMeta } from "./merge-meta"; | |
export const meta = mergeMeta( | |
// these will override the parent meta | |
({ data }) => { | |
return [{ title: data.project.name }]; | |
}, | |
// these will be appended to the parent meta | |
({ matches }) => { | |
return [{ name: "author", content: "Ryan Florence" }]; | |
}, | |
); | |
// both functions get the same arguments as a normal meta function |
How would I go about using this similar to the loader data to have the types available?
This works in my IDE, but throws an error on the server: You cannot
useLoaderData
in an errorElement (routeId: root)
({ data }: V2_ServerRuntimeMetaArgs<typeof loader>) => {
This is an important feature, at least for me. Is this likely to be included in the core library? If not, I may publish an improved version in remix-utils
.
The above mergeMeta
will run into issues when you get to deeper nested routes that attempt to override the same meta tags. This is because the mergedMeta
reducer will concatenate the meta from all the parent routes. So if you have any duplicates in the parent routes, or attempt to override the same tag in different routes, they will be duplicated in the merged meta. And if you try and override the duplicate meta, it will only override the first (closer to the root) instance of that tag.
This is a problem because the <Meta />
component uses a JSON string of the meta props. So when we have duplicate tags React complains about duplicate keys.
A better solution would be to only merge the meta from the direct parent route of the current route.
The output of matches should always be ordered such that root route -> parent route -> child route
. So we can simply changing the following code:
return arg => {
// get meta from parent routes
- let mergedMeta = arg.matches.reduce((acc, match) => {
- return acc.concat(match.meta || []);
+ let mergedMeta = arg.matches.reduceRight((acc, match) => {
+ if (match.meta.length > 0 && acc.length == 0)
+ return acc.concat(match.meta || [])
+ return acc
}, [] as V2_HtmlMetaDescriptor[]);
// replace any parent meta with the same name or property with the override
This still has issues though, because you can still append meta using the appendFn and cause duplicates. To solve this you could check that you're not overriding any tags when appending, or just be careful when appending tags 🤷 This becomes tedious on larger projects.
An alternative solution is to provide only one function that takes precedence over the parent meta:
// NOTE: The type MetaFunction here is the v2 version
export const mergeMeta = <
Loader extends LoaderFunction | unknown = unknown,
ParentsLoaders extends Record<string, LoaderFunction | unknown> = Record<
string,
unknown
>
>(
leafMetaFn: MetaFunction<Loader, ParentsLoaders>
): MetaFunction<Loader, ParentsLoaders> => {
return (arg) => {
let leafMeta = leafMetaFn(arg)
return arg.matches.reduceRight((acc, match) => {
for (let parentMeta of match.meta) {
let index = acc.findIndex(
(meta) =>
('name' in meta &&
'name' in parentMeta &&
meta.name === parentMeta.name) ||
('property' in meta &&
'property' in parentMeta &&
meta.property === parentMeta.property) ||
('title' in meta && 'title' in parentMeta)
)
if (index == -1) {
// Parent meta not found in acc, so add it
acc.push(parentMeta)
}
}
return acc
}, leafMeta)
}
}
This flips the original mergeMeta
on it's head by assuming you always want the added leaf meta tags, and only including parent tags if they're not a duplicate - giving precendence to tags in routes furthest from the root.
This also duplicates the generic typing of MetaFunction
so that you can pass your loader type through, so usage now looks like this:
export const meta: MetaFunction<typeof loader> = mergeMeta(({ data }) => [
// data is now typed based on your loader
{ title: data.project.name },
{ name: "author", content: "Ryan Florence" }
])
And as before leafMetaFn
takes the same arguments as MetaFunction
.
Thank you for your code @cairin, it works great, however it seems like you've forgotten about ('script:ld+json' in meta && 'script:ld+json' in parentMeta)
, since it's a possibility for merging too.
Thank you for your code @cairin, it works great, however it seems like you've forgotten about
('script:ld+json' in meta && 'script:ld+json' in parentMeta)
, since it's a possibility for merging too.
You wouldn't want to deduplicate scripts like that, because that would deduplicate different scripts too. You would need to do a full object comparison. It's probably more useful for you to add that code yourself based on your specific requirements.
You also shouldn't need to install the same script in multiple routes anyway, as long as it's installed in a route somewhere above the current route it should still be usable on the leaf route.
@cairin since TS 5.5 mergeMeta
cannot infer data
correctly.
@cairin since TS 5.5
mergeMeta
cannot inferdata
correctly.
It appears that the issue is unique to my project. I upgraded TypeScript to version 5.5.2 in a newly installed Remix app without any problems. However, comparing both tsconfig files hasn't resolved the issue. @cairin mergeMeta
function version works with latest Remix app and TS. 🤷♂️
How would I go about using this similar to the loader data to have the types available?
I've tried this:
This works in my IDE, but throws an error on the server: You cannot
useLoaderData
in an errorElement (routeId: root)