Skip to content

Commit 889e61a

Browse files
authored
feat(core)!: default capo sorting (#440)
* feat!: default capo sorting * chore: fixing tests * chore: fixing tests * chore: simplify
1 parent 838a713 commit 889e61a

File tree

18 files changed

+119
-258
lines changed

18 files changed

+119
-258
lines changed

docs/content/1.usage/2.guides/2.sorting.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ title: Tag Sorting
33
description: How tags are sorted and how to configure them.
44
---
55

6-
Once tags are [deduped](/usage/guides/handling-duplicates), they will be sorted. Sorting the tags is important
7-
to ensure critical tags are rendered first, as well as allowing you to have tags in a specific order that you need them in.
6+
## Introduction
87

9-
For example, if you need to preload an asset, you'll need this to come before the asset itself. Which is a bit of a challenge
10-
when the tags are nested.
8+
Sorting the tags is important to ensure critical tags are rendered first, as well as allowing you to have tags in a specific order that you need them in.
119

12-
## Sorting Logic
10+
## Tag Sorting Logic
11+
12+
Sorting is first done using the [Capo.js](https://rviscomi.github.io/capo.js/) weights, making sure tags are rendered in
13+
a specific way to avoid [Critical Request Chains](https://web.dev/critical-request-chains/) issues as well
14+
as rendering bugs.
1315

1416
Sorting is done in multiple steps:
1517
- order critical tags first

docs/content/3.plugins/plugins/capo.md

Lines changed: 0 additions & 45 deletions
This file was deleted.

packages/schema/src/head.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ export interface CreateHeadOptions {
8181
document?: Document
8282
plugins?: HeadPluginInput[]
8383
hooks?: NestedHooks<HeadHooks>
84+
/**
85+
* Disable the Capo.js tag sorting algorithm.
86+
*
87+
* This is added to make the v1 -> v2 migration easier allowing users to opt-out of the new sorting algorithm.
88+
*/
89+
disableCapoSorting?: boolean
8490
}
8591

8692
export interface HeadEntryOptions extends TagPosition, TagPriority, ProcessesTemplateParams, ResolvesDuplicates {

packages/shared/src/sort.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { HeadTag } from '@unhead/schema'
1+
import type { HeadTag, Unhead } from '@unhead/schema'
22

33
export const TAG_WEIGHTS = {
44
// tags
@@ -13,10 +13,18 @@ export const TAG_ALIASES = {
1313
low: 20,
1414
} as const
1515

16-
export function tagWeight<T extends HeadTag>(tag: T) {
16+
export const SortModifiers = [{ prefix: 'before:', offset: -1 }, { prefix: 'after:', offset: 1 }]
17+
18+
const importRe = /@import/
19+
const isTruthy = (val?: string | boolean) => val === '' || val === true
20+
21+
export function tagWeight<T extends HeadTag>(head: Unhead<any>, tag: T) {
1722
const priority = tag.tagPriority
1823
if (typeof priority === 'number')
1924
return priority
25+
const isScript = tag.tag === 'script'
26+
const isLink = tag.tag === 'link'
27+
const isStyle = tag.tag === 'style'
2028
let weight = 100
2129
if (tag.tag === 'meta') {
2230
// CSP needs to be as it effects the loading of assets
@@ -28,7 +36,7 @@ export function tagWeight<T extends HeadTag>(tag: T) {
2836
else if (tag.props.name === 'viewport')
2937
weight = -15
3038
}
31-
else if (tag.tag === 'link' && tag.props.rel === 'preconnect') {
39+
else if (isLink && tag.props.rel === 'preconnect') {
3240
// preconnects should almost always come first
3341
weight = 20
3442
}
@@ -39,6 +47,38 @@ export function tagWeight<T extends HeadTag>(tag: T) {
3947
// @ts-expect-e+rror untyped
4048
return weight + TAG_ALIASES[priority as keyof typeof TAG_ALIASES]
4149
}
50+
if (tag.tagPosition && tag.tagPosition !== 'head') {
51+
return weight
52+
}
53+
if (!head.ssr || head.resolvedOptions.disableCapoSorting) {
54+
return weight
55+
}
56+
if (isScript && isTruthy(tag.props.async)) {
57+
// ASYNC_SCRIPT
58+
weight = 30
59+
// SYNC_SCRIPT
60+
}
61+
else if (isStyle && tag.innerHTML && importRe.test(tag.innerHTML)) {
62+
// IMPORTED_STYLES
63+
weight = 40
64+
}
65+
else if (isScript && tag.props.src && !isTruthy(tag.props.defer) && !isTruthy(tag.props.async) && tag.props.type !== 'module' && !tag.props.type?.endsWith('json')) {
66+
weight = 50
67+
}
68+
else if ((isLink && tag.props.rel === 'stylesheet') || tag.tag === 'style') {
69+
// SYNC_STYLES
70+
weight = 60
71+
}
72+
else if (isLink && (tag.props.rel === 'preload' || tag.props.rel === 'modulepreload')) {
73+
// PRELOAD
74+
weight = 70
75+
}
76+
else if (isScript && isTruthy(tag.props.defer) && tag.props.src && !isTruthy(tag.props.async)) {
77+
// DEFER_SCRIPT
78+
weight = 80
79+
}
80+
else if (isLink && (tag.props.rel === 'prefetch' || tag.props.rel === 'dns-prefetch' || tag.props.rel === 'prerender')) {
81+
weight = 90
82+
}
4283
return weight
4384
}
44-
export const SortModifiers = [{ prefix: 'before:', offset: -1 }, { prefix: 'after:', offset: 1 }]

packages/unhead/export-size-report.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,6 @@
4141
"minzipped": 2151,
4242
"bundled": 11435
4343
},
44-
{
45-
"name": "CapoPlugin",
46-
"path": "dist/index.mjs",
47-
"minified": 5113,
48-
"minzipped": 1891,
49-
"bundled": 10303
50-
},
5144
{
5245
"name": "useServerSeoMeta",
5346
"path": "dist/index.mjs",

packages/unhead/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,3 @@ export * from './composables/useServerHead'
1919
export * from './composables/useServerHeadSafe'
2020
export * from './composables/useServerSeoMeta'
2121
export * from './context'
22-
export * from './optionalPlugins/capoPlugin'

packages/unhead/src/optionalPlugins/capoPlugin.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

packages/unhead/src/plugins/dedupe.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { defineHeadPlugin, HasElementTags, hashTag, tagDedupeKey, tagWeight } fr
33

44
const UsesMergeStrategy = new Set(['templateParams', 'htmlAttrs', 'bodyAttrs'])
55

6-
export default defineHeadPlugin({
6+
export default defineHeadPlugin(head => ({
77
hooks: {
88
'tag:normalise': ({ tag }) => {
99
// support for third-party dedupe keys
@@ -74,7 +74,7 @@ export default defineHeadPlugin({
7474
dupedTag._duped.push(tag)
7575
continue
7676
}
77-
else if (tagWeight(tag) > tagWeight(dupedTag)) {
77+
else if ((!tag.key || !dupedTag.key) && tagWeight(head, tag) > tagWeight(head, dupedTag)) {
7878
// check tag weights
7979
continue
8080
}
@@ -110,4 +110,4 @@ export default defineHeadPlugin({
110110
.filter(t => !(t.tag === 'meta' && (t.props.name || t.props.property) && !t.props.content))
111111
},
112112
},
113-
})
113+
}))

packages/unhead/src/plugins/sort.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defineHeadPlugin, SortModifiers, tagWeight } from '@unhead/shared'
22

3-
export default defineHeadPlugin({
3+
4+
export default defineHeadPlugin((head => ({
45
hooks: {
56
'tags:resolve': (ctx) => {
67
// 2a. Sort based on priority
@@ -17,18 +18,20 @@ export default defineHeadPlugin({
1718

1819
const key = (tag.tagPriority as string).substring(prefix.length)
1920

20-
const position = ctx.tags.find(tag => tag._d === key)?._p
21-
22-
if (position !== undefined) {
23-
tag._p = position + offset
21+
const linkedTag = ctx.tags.find(tag => tag._d === key)
22+
if (linkedTag) {
23+
if (typeof linkedTag?.tagPriority === 'number') {
24+
tag.tagPriority = linkedTag.tagPriority
25+
}
26+
tag._p = linkedTag._p! + offset
2427
break
2528
}
2629
}
2730
}
2831

2932
ctx.tags.sort((a, b) => {
30-
const aWeight = tagWeight(a)
31-
const bWeight = tagWeight(b)
33+
const aWeight = tagWeight(head, a)
34+
const bWeight = tagWeight(head, b)
3235

3336
// 2c. sort based on critical tags
3437
if (aWeight < bWeight) {
@@ -43,4 +46,4 @@ export default defineHeadPlugin({
4346
})
4447
},
4548
},
46-
})
49+
})))

packages/vue/export-size-report.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,6 @@
112112
"minzipped": 356,
113113
"bundled": 1618
114114
},
115-
{
116-
"name": "CapoPlugin",
117-
"path": "dist/index.mjs",
118-
"minified": 179,
119-
"minzipped": 132,
120-
"bundled": 460
121-
},
122115
{
123116
"name": "createHeadCore",
124117
"path": "dist/index.mjs",

0 commit comments

Comments
 (0)