---
title: "Tag Sorting & Placement"
description: "Control where head tags render with tagPosition (head, bodyOpen, bodyClose) and tagPriority for ordering. Understand the Capo.js weights applied during SSR."
canonical_url: "https://unhead.unjs.io/docs/head/guides/core-concepts/positions"
last_updated: "2026-08-11T00:46:01.856Z"
---

Use `tagPosition: 'head' | 'bodyOpen' | 'bodyClose'` to choose a render location. Use `tagPriority: 'critical' | 'high' | number | 'low'` to influence order within a location.

## Tag placement

The `<link>`, `<script>`, `<noscript>`, and `<style>` inputs accept an optional `tagPosition` property:

- `head`: Render in the `<head>` (default)
- `bodyOpen`: Render at the start of the `<body>`
- `bodyClose`: Render at the end of the `<body>`

<note>

Placing a classic synchronous script at `bodyClose` lets the parser build the preceding body first. A script without `async`, `defer`, or `type="module"` still pauses parsing when the browser reaches it. See [MDN's script loading reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#notes).

</note>

### Placement examples

- **Analytics scripts**: Placing a script at `bodyClose` lets the parser process the preceding body markup first, although downloading and executing the script still has a cost
- **Critical CSS**: Place essential styles in `head` with high priority
- **Early body scripts**: Use `bodyOpen` only when a script must run before the rest of the body; a classic synchronous script there still blocks parsing

```ts
import { useHead } from '@unhead/dynamic-import'

// useHead: /docs/head/api/composables/use-head
useHead({
  script: [
    {
      src: '/my-lazy-script.js',
      tagPosition: 'bodyClose',
    },
  ],
})
// renders
//   ...
//   <script src="/my-lazy-script.js"></script>
// </body>
```

## Server and client sort order

During server rendering, each tag receives a weight. Lower weights render first.

[Capo.js](https://rviscomi.github.io/capo.js/) weights are automatically applied to server-rendered tags to reduce [critical request chains](https://developer.chrome.com/docs/lighthouse/performance/critical-request-chains). The client applies explicit numeric and alias priorities, but does not recompute Capo ordering. The server weights are:

- **-30**: `<meta http-equiv="content-security-policy" ...>`
- **-20**: `<meta charset ...>`
- **-15**: `<meta name="viewport" ...>`
- **-10**: `<base>`
- **10**: `<title>`
- **20**: `<link rel="preconnect" ...>`
- **25**: `<script type="importmap">`
- **30**: `<script async ...>`
- **40**: `<style>@import ...</style>`
- **50**: sync `<script>` (inline or `src`)
- **60**: `<style>`, `<link rel="stylesheet" ...>`
- **70**: `<link rel="preload" ...>`, `<link rel="modulepreload" ...>`
- **80**: `<script defer ...>`, `<script type="module" ...>`
- **90**: `<script type="speculationrules">`, `<link rel="prefetch" ...>`, `<link rel="dns-prefetch" ...>`, `<link rel="prerender" ...>`

All other tags have a default priority of `100`.

<note>

`<script type="importmap">` is pinned at weight 25 so it is emitted before async scripts, module scripts, and `modulepreload`. The [HTML Standard's import-map processing model](https://html.spec.whatwg.org/multipage/webappapis.html#import-maps) allows multiple maps and prevents later maps from changing module resolutions that already occurred. Unhead therefore does not force-dedupe import maps. Set an explicit `key` for last-wins replacement instead.

</note>

Override these default weights with the `tagPriority` property.

### Setting tag priority

The `tagPriority` property can be set to an explicit weight, a string alias or a string to target a specific tag.

#### Sorting with Aliases

Aliases adjust a tag's current weight instead of replacing it. The server starts from its Capo.js weight; the client starts from a baseline of 100:

- `critical`: subtracts **8**
- `high`: subtracts **1**
- `low`: adds **2**

```ts
import { useHead } from '@unhead/dynamic-import'

useHead({
  script: [
    {
      src: '/my-lazy-script.js',
      tagPriority: 'low',
    },
  ],
})
```

#### Sort by number

When providing a number, refer to the priorities set for critical tags above.

```ts
import { useHead } from '@unhead/dynamic-import'

// A layout registers a script
useHead({
  script: [
    {
      src: '/not-important-script.js',
    },
  ],
})

// but in our page we want to run a script before the above
useHead({
  script: [
    {
      src: '/very-important-script.js',
      tagPriority: 0,
    },
  ],
})

// <script src=\"/very-important-script.js\"></script>
// <script src=\"/not-important-script.js\"></script>
```

#### Sort with `before:` and `after:`

To place one tag relative to another, use the optional [Alias Sorting Plugin](/docs/head/guides/plugins/alias-sorting).

### Custom server weights

For custom ordering, pass a `tagWeight` function when creating the server head. It receives each tag and returns a numeric weight; lower values render first.

The default `capoTagWeight` function is exported from `unhead/server` so you can wrap it:

```ts
import { createHead, capoTagWeight } from '@unhead/dynamic-import/server'

const head = createHead({
  tagWeight(tag) {
    // Promote SEO meta above styles for bot requests
    if (isBot && tag.tag === 'meta' && tag.props.property?.startsWith('og:'))
      return 55 // just above styles (60)
    return capoTagWeight(tag)
  }
})
```

The function can vary ordering by request context. For example, it can promote social metadata for crawler requests while retaining CAPO order for browsers.

<tip>

The [Validate Plugin](/docs/head/guides/plugins/validate) includes a `meta-beyond-1mb` heuristic that warns when large inline content pushes metadata beyond its default 1MB inspection threshold.

</tip>

### Preserved order during hydration

When hydrating the state (e.g., SSR or page switch), Unhead replaces existing tags in their current position to avoid a flash of content.

This may cause `tagPriority` to be ignored during hydration. For client-side-only applications or SPAs, this isn't an issue, but for SSR applications, be aware that the initial render positions may be preserved during hydration.

## Programmatic reordering

The `tags:afterResolve` hook receives tags after resolution and before DOM rendering. Modify `ctx.tags` when `tagPriority` is not expressive enough:

```ts
import { defineHeadPlugin } from '@unhead/dynamic-import/plugins'

export const fontPreloadFirstPlugin = defineHeadPlugin({
  key: 'font-preload-first',
  hooks: {
    'tags:afterResolve': (ctx) => {
      const fontPreloads = ctx.tags.filter(tag =>
        tag.tag === 'link'
        && tag.props.rel === 'preload'
        && tag.props.as === 'font'
      )
      const otherTags = ctx.tags.filter(tag =>
        !(tag.tag === 'link'
          && tag.props.rel === 'preload'
          && tag.props.as === 'font')
      )
      ctx.tags = [...fontPreloads, ...otherTags]
    }
  }
})
```

## See Also

- [Alias Sorting Plugin](/docs/head/guides/plugins/alias-sorting): Sort tags by alias
- [Handling Duplicates](/docs/head/guides/core-concepts/handling-duplicates): Deduplication strategies
- [useHead() API](/docs/head/api/composables/use-head): `tagPriority` option
