Skip to content

Commit

Permalink
feat(core)!: default capo sorting (#440)
Browse files Browse the repository at this point in the history
* feat!: default capo sorting

* chore: fixing tests

* chore: fixing tests

* chore: simplify
  • Loading branch information
harlan-zw authored Jan 7, 2025
1 parent 838a713 commit 889e61a
Show file tree
Hide file tree
Showing 18 changed files with 119 additions and 258 deletions.
12 changes: 7 additions & 5 deletions docs/content/1.usage/2.guides/2.sorting.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ title: Tag Sorting
description: How tags are sorted and how to configure them.
---

Once tags are [deduped](/usage/guides/handling-duplicates), they will be sorted. 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.
## Introduction

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
when the tags are nested.
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.

## Sorting Logic
## Tag Sorting Logic

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

Sorting is done in multiple steps:
- order critical tags first
Expand Down
45 changes: 0 additions & 45 deletions docs/content/3.plugins/plugins/capo.md

This file was deleted.

6 changes: 6 additions & 0 deletions packages/schema/src/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export interface CreateHeadOptions {
document?: Document
plugins?: HeadPluginInput[]
hooks?: NestedHooks<HeadHooks>
/**
* Disable the Capo.js tag sorting algorithm.
*
* This is added to make the v1 -> v2 migration easier allowing users to opt-out of the new sorting algorithm.
*/
disableCapoSorting?: boolean
}

export interface HeadEntryOptions extends TagPosition, TagPriority, ProcessesTemplateParams, ResolvesDuplicates {
Expand Down
48 changes: 44 additions & 4 deletions packages/shared/src/sort.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { HeadTag } from '@unhead/schema'
import type { HeadTag, Unhead } from '@unhead/schema'

export const TAG_WEIGHTS = {
// tags
Expand All @@ -13,10 +13,18 @@ export const TAG_ALIASES = {
low: 20,
} as const

export function tagWeight<T extends HeadTag>(tag: T) {
export const SortModifiers = [{ prefix: 'before:', offset: -1 }, { prefix: 'after:', offset: 1 }]

const importRe = /@import/
const isTruthy = (val?: string | boolean) => val === '' || val === true

export function tagWeight<T extends HeadTag>(head: Unhead<any>, tag: T) {
const priority = tag.tagPriority
if (typeof priority === 'number')
return priority
const isScript = tag.tag === 'script'
const isLink = tag.tag === 'link'
const isStyle = tag.tag === 'style'
let weight = 100
if (tag.tag === 'meta') {
// CSP needs to be as it effects the loading of assets
Expand All @@ -28,7 +36,7 @@ export function tagWeight<T extends HeadTag>(tag: T) {
else if (tag.props.name === 'viewport')
weight = -15
}
else if (tag.tag === 'link' && tag.props.rel === 'preconnect') {
else if (isLink && tag.props.rel === 'preconnect') {
// preconnects should almost always come first
weight = 20
}
Expand All @@ -39,6 +47,38 @@ export function tagWeight<T extends HeadTag>(tag: T) {
// @ts-expect-e+rror untyped
return weight + TAG_ALIASES[priority as keyof typeof TAG_ALIASES]
}
if (tag.tagPosition && tag.tagPosition !== 'head') {
return weight
}
if (!head.ssr || head.resolvedOptions.disableCapoSorting) {
return weight
}
if (isScript && isTruthy(tag.props.async)) {
// ASYNC_SCRIPT
weight = 30
// SYNC_SCRIPT
}
else if (isStyle && tag.innerHTML && importRe.test(tag.innerHTML)) {
// IMPORTED_STYLES
weight = 40
}
else if (isScript && tag.props.src && !isTruthy(tag.props.defer) && !isTruthy(tag.props.async) && tag.props.type !== 'module' && !tag.props.type?.endsWith('json')) {
weight = 50
}
else if ((isLink && tag.props.rel === 'stylesheet') || tag.tag === 'style') {
// SYNC_STYLES
weight = 60
}
else if (isLink && (tag.props.rel === 'preload' || tag.props.rel === 'modulepreload')) {
// PRELOAD
weight = 70
}
else if (isScript && isTruthy(tag.props.defer) && tag.props.src && !isTruthy(tag.props.async)) {
// DEFER_SCRIPT
weight = 80
}
else if (isLink && (tag.props.rel === 'prefetch' || tag.props.rel === 'dns-prefetch' || tag.props.rel === 'prerender')) {
weight = 90
}
return weight
}
export const SortModifiers = [{ prefix: 'before:', offset: -1 }, { prefix: 'after:', offset: 1 }]
7 changes: 0 additions & 7 deletions packages/unhead/export-size-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,6 @@
"minzipped": 2151,
"bundled": 11435
},
{
"name": "CapoPlugin",
"path": "dist/index.mjs",
"minified": 5113,
"minzipped": 1891,
"bundled": 10303
},
{
"name": "useServerSeoMeta",
"path": "dist/index.mjs",
Expand Down
1 change: 0 additions & 1 deletion packages/unhead/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ export * from './composables/useServerHead'
export * from './composables/useServerHeadSafe'
export * from './composables/useServerSeoMeta'
export * from './context'
export * from './optionalPlugins/capoPlugin'
59 changes: 0 additions & 59 deletions packages/unhead/src/optionalPlugins/capoPlugin.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/unhead/src/plugins/dedupe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineHeadPlugin, HasElementTags, hashTag, tagDedupeKey, tagWeight } fr

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

export default defineHeadPlugin({
export default defineHeadPlugin(head => ({
hooks: {
'tag:normalise': ({ tag }) => {
// support for third-party dedupe keys
Expand Down Expand Up @@ -74,7 +74,7 @@ export default defineHeadPlugin({
dupedTag._duped.push(tag)
continue
}
else if (tagWeight(tag) > tagWeight(dupedTag)) {
else if ((!tag.key || !dupedTag.key) && tagWeight(head, tag) > tagWeight(head, dupedTag)) {
// check tag weights
continue
}
Expand Down Expand Up @@ -110,4 +110,4 @@ export default defineHeadPlugin({
.filter(t => !(t.tag === 'meta' && (t.props.name || t.props.property) && !t.props.content))
},
},
})
}))
19 changes: 11 additions & 8 deletions packages/unhead/src/plugins/sort.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineHeadPlugin, SortModifiers, tagWeight } from '@unhead/shared'

export default defineHeadPlugin({

export default defineHeadPlugin((head => ({
hooks: {
'tags:resolve': (ctx) => {
// 2a. Sort based on priority
Expand All @@ -17,18 +18,20 @@ export default defineHeadPlugin({

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

const position = ctx.tags.find(tag => tag._d === key)?._p

if (position !== undefined) {
tag._p = position + offset
const linkedTag = ctx.tags.find(tag => tag._d === key)
if (linkedTag) {
if (typeof linkedTag?.tagPriority === 'number') {
tag.tagPriority = linkedTag.tagPriority
}
tag._p = linkedTag._p! + offset
break
}
}
}

ctx.tags.sort((a, b) => {
const aWeight = tagWeight(a)
const bWeight = tagWeight(b)
const aWeight = tagWeight(head, a)
const bWeight = tagWeight(head, b)

// 2c. sort based on critical tags
if (aWeight < bWeight) {
Expand All @@ -43,4 +46,4 @@ export default defineHeadPlugin({
})
},
},
})
})))
7 changes: 0 additions & 7 deletions packages/vue/export-size-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,6 @@
"minzipped": 356,
"bundled": 1618
},
{
"name": "CapoPlugin",
"path": "dist/index.mjs",
"minified": 179,
"minzipped": 132,
"bundled": 460
},
{
"name": "createHeadCore",
"path": "dist/index.mjs",
Expand Down
7 changes: 1 addition & 6 deletions packages/vue/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CapoPlugin, createHeadCore, unheadCtx } from 'unhead'
import { createHeadCore, unheadCtx } from 'unhead'
import { createHead, createServerHead } from './createHead'
import { resolveUnrefHeadInput } from './utils'

Expand All @@ -10,11 +10,6 @@ export {
unheadCtx,
}

// extra plugins
export {
CapoPlugin,
}

// utils
export {
resolveUnrefHeadInput,
Expand Down
4 changes: 2 additions & 2 deletions packages/vue/test/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ describe('vue e2e', () => {
"headTags": "<meta charset="utf-8">
<title>Home</title>
<script src="https://analytics.example.com/script.js" defer async></script>
<script src="https://my-app.com/home.js"></script>
<meta property="og:title" content="My amazing site">
<meta property="og:description" content="This is my amazing site">
<meta property="og:locale" content="en_US">
<meta property="og:locale" content="en_AU">
<meta property="og:image" content="https://cdn.example.com/image.jpg">
<meta property="og:image" content="https://cdn.example.com/image2.jpg">
<script src="https://my-app.com/home.js"></script>
<meta name="description" content="This is the home page">
<script id="unhead:payload" type="application/json">{"title":"My amazing site"}</script>",
"htmlAttrs": " lang="en"",
Expand Down Expand Up @@ -131,13 +131,13 @@ describe('vue e2e', () => {
<meta charset="utf-8">
<title>Home</title>
<script src="https://analytics.example.com/script.js" defer="" async=""></script>
<script src="https://my-app.com/home.js"></script>
<meta property="og:title" content="Home">
<meta property="og:description" content="This is my amazing site">
<meta property="og:locale" content="en_US">
<meta property="og:locale" content="en_AU">
<meta property="og:image" content="https://cdn.example.com/image.jpg">
<meta property="og:image" content="https://cdn.example.com/image2.jpg">
<script src="https://my-app.com/home.js"></script>
<meta name="description" content="This is the home page">
<script id="unhead:payload" type="application/json">{"title":"My amazing site"}</script>
</head>
Expand Down
8 changes: 3 additions & 5 deletions packages/vue/test/ssr/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,13 @@ describe('vue ssr examples', () => {
})
})

expect(headResult.headTags).toMatchInlineSnapshot(
`
expect(headResult.headTags).toMatchInlineSnapshot(`
"<title>hello</title>
<script src="foo.js"></script>
<meta property="og:locale:alternate" content="fr">
<meta property="og:locale:alternate" content="zh">
<script src="foo.js"></script>
<meta name="description" content="desc 2">"
`,
)
`)
expect(headResult.htmlAttrs).toEqual(' lang="zh"')
})

Expand Down
Loading

0 comments on commit 889e61a

Please sign in to comment.