Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/0.react/1.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function render(_url: string) {
{
htmlAttrs: { lang: 'en' },
title: 'Default title',
titleTemplate: '%s %separator My Site',
titleTemplate: '%s - My Site',
},
]
})
Expand Down Expand Up @@ -178,14 +178,14 @@ export default function App() {
If you're using [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import), you can automatically import the composables.

```ts [vite.config.ts]
import { unheadReactComposablesImports } from '@unhead/react'
import { hookImports } from '@unhead/react'
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
plugins: [
AutoImport({
imports: [
unheadReactComposablesImports,
hookImports,
],
}),
// ...
Expand Down
200 changes: 200 additions & 0 deletions docs/0.svelte/1.installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
---
title: Installing Unhead with Svelte
description: Learn how to start using Unhead with Svelte.
navigation:
title: 'Installation'
---

## Introduction

Unhead has first-class support for Svelte, improving the developer experience and performance of using head tags in your app.

It provides advanced features in comparison to Svelte's in built `<svelte:head>`{lang="html"} component, supporting a more diverse set of use cases from SEO, structured data and
script loading.

It's designed to work with any Svelte setup, however this guide assumes you're following a similar structure to the [Vite: ssr-svelte-ts](https://github.com/bluwy/create-vite-extra/tree/master/template-ssr-Svelte-ts) template
or a Vite SPA.

### Demos

- [StackBlitz - Unhead - Vite + Svelte SSR](https://stackblitz.com/edit/github-5hqsxyid)
- [StackBlitz - Unhead - Svelte SPA](https://stackblitz.com/edit/vitejs-vite-ggqxj5nx)

## Setup

### 1. Add Dependency

Install `@unhead/svelte`{lang="bash"} dependency to your project. The `next` tag is for v2 of Unhead which is required for Svelte.

:ModuleInstall{name="@unhead/svelte@next"}

### 2. Setup Client-Side Rendering

To begin with, we'll import the function to initialize Unhead in our _client_ Svelte app from `@unhead/svelte/client`{lang="bash"}.

In Vite this entry file is typically named `entry-client.ts`{lang="bash"}. If you're not server-side rendering, you can add this to your main Svelte app entry instead.

```tsx {1,7,12,14} [src/entry-client.ts]
import './app.css'
import { hydrate } from 'svelte'
import App from './App.svelte'
import { createHead, UnheadContextKey } from '@unhead/svelte/client'

const unhead = createHead()
const context = new Map()
context.set(UnheadContextKey, unhead)

hydrate(App, {
target: document.getElementById('app')!,
context: context
})
```

### 3. Setup Server-Side Rendering

::note
Serving your app as an SPA? You can [skip](/docs/svelte/installation#_4-your-first-tags) this step.
::

Setting up server-side rendering is more complicated as it requires rendering out the tags to the HTML string before sending it to the client.

We'll start with setting up the plugin in the _server_ entry this time. Make sure to import from `@unhead/svelte/server`{lang="bash"} instead
and add the `head` in the return object.

```tsx {1,7,10,12,15} [src/entry-server.ts]
import { render as _render } from 'svelte/server'
import App from './App.svelte'
import { createHead, UnheadContextKey } from '@unhead/svelte/server'

export function render(_url: string) {
const unhead = createHead()
const context = new Map()
context.set(UnheadContextKey, unhead)
return {
render: _render(App, {
context,
}),
unhead,
}
}
```

Now we need to render out the head tags _after_ Svelte has rendered the app.

Within your `server.js` file or wherever you're handling the template logic, you need to transform the template data
for the head tags using `transformHtmlTemplate()`{lang="ts"}.

```ts {1,9-14} [server.ts]
import { transformHtmlTemplate } from '@unhead/svelte/server'
// ...

// Serve HTML
app.use('*all', async (req, res) => {
try {
// ...

const rendered = await render(url)
const html = await transformHtmlTemplate(
rendered.unhead,
template
.replace(`<!--app-head-->`, rendered.head ?? '')
.replace(`<!--app-html-->`, rendered.html ?? '')
)

res.status(200).set({ 'Content-Type': 'text/html' }).send(html)
}
catch (e) {
// ...
}
})
// ..
```

### 4. Your First Tags

Done! Your app should now be rendering head tags on the server and client.

To improve your apps stability, Unhead will now insert important default tags for you.

- `<meta charset="utf-8">`
- `<meta name="viewport" content="width=device-width, initial-scale=1">`
- `<html lang="en">`

You may need to change these for your app requirements, for example you may want to change the default language. Adding
tags in your server entry means you won't add any weight to your client bundle.

```ts {2,6-8} [src/entry-server.ts]
import { createHead } from '@unhead/svelte/server'

export function render(_url: string) {
const head = createHead({
// change default initial lang
init: [
{
htmlAttrs: { lang: 'en' },
title: 'Default title',
titleTemplate: '%s - My Site',
},
]
})
const html = `<!-- your html -->`
return { html, head }
}
```

For adding tags in your components, you can use the `useHead()`{lang="ts"} or `useSeoMeta()`{lang="ts"} composables.

```tsx [App.svelte]
<script lang="ts">
import { useHead, useHead } from '@unhead/Svelte'

// a.
useHead({
title: 'My Awesome Site',
meta: [
{ name: 'description', content: 'My awesome site description' }
]
})

// b.
useSeoMeta({
title: 'My Awesome Site',
description: 'My awesome site description'
})
</script>
```

For handling reactive input, check out the [Reactivity](/docs/svelte/guides/reactivity) guide.

### 5. Optional: Auto-Imports

If you're using [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import), you can automatically import the composables.

```ts [vite.config.ts]
import { autoImports } from '@unhead/svelte'
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
plugins: [
AutoImport({
imports: [
autoImports,
],
}),
// ...
]
})
```

## Next Steps

Your Svelte app is now setup for head management, congrats! πŸŽ‰

You can get started with [reactive input](/docs/svelte/guides/reactivity) for any of the hooks or components:
- [`useHead()`{lang="ts"}](/docs/api/use-head)
- [`useSeoMeta()`{lang="ts"}](/docs/api/use-seo-meta)

Or explore some of the optional extras:

- Add [`useSchemaOrg()`{lang="ts"}](/docs/api/use-schema-org) for structured data
- Use [`useScript()`{lang="ts"}](/docs/api/use-script) for performance optimized script loading
35 changes: 35 additions & 0 deletions docs/0.svelte/guides/0.reactivity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Reactivity
description: Learn how to handle state changes for head tags in Svelte.
---

## Introduction

Unhead works in Svelte by attaching to the Svelte context. This allows you to manage head tags across your app with ease.

Svelte does not support reactivity outside of components, to support reactive tags requires slightly different syntax.

## Using $effect

To make sure state changes will trigger an update to our tags, we need to initialise our head entry using `useHead()`
and use the provided `patch()` function within a Svelte `$effect`.

```sveltehtml
<script lang="ts">

let title = $state('hello world')

const head = useHead()

$effect(() => {
head.path({
title,
})
})

</script>

<button onclick={title = 'Updated title'}>update title</button>
```

While we could technically call `useHead()` within the `$effect`, this is the best practice to avoid unneeded memory allocations.
10 changes: 10 additions & 0 deletions examples/vite-ssr-svelte/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<!--app-head-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/entry-client.ts"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions examples/vite-ssr-svelte/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "vite-svelte-ts-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "node server",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.ts --outDir dist/server",
"preview": "cross-env NODE_ENV=production node server",
"check": "svelte-check"
},
"dependencies": {
"compression": "^1.7.5",
"express": "^5.0.1",
"sirv": "^3.0.0",
"@unhead/svelte": "workspace:*"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.1",
"@tsconfig/svelte": "^5.0.4",
"@types/express": "^5.0.0",
"@types/node": "^22.10.0",
"cross-env": "^7.0.3",
"svelte": "^5.2.9",
"svelte-check": "^4.1.0",
"tslib": "^2.8.1",
"typescript": "~5.7.2",
"vite": "^6.0.1"
}
}
74 changes: 74 additions & 0 deletions examples/vite-ssr-svelte/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import fs from 'node:fs/promises'
import express from 'express'
import { transformHtmlTemplate } from "@unhead/svelte/server";

// Constants
const isProduction = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 5173
const base = process.env.BASE || '/'

// Cached production assets
const templateHtml = isProduction
? await fs.readFile('./dist/client/index.html', 'utf-8')
: ''

// Create http server
const app = express()

// Add Vite or respective production middlewares
/** @type {import('vite').ViteDevServer | undefined} */
let vite
if (!isProduction) {
const { createServer } = await import('vite')
vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
base,
})
app.use(vite.middlewares)
} else {
const compression = (await import('compression')).default
const sirv = (await import('sirv')).default
app.use(compression())
app.use(base, sirv('./dist/client', { extensions: [] }))
}

// Serve HTML
app.use('*all', async (req, res) => {
try {
const url = req.originalUrl.replace(base, '')

/** @type {string} */
let template
/** @type {import('./src/entry-server.ts').render} */
let render
if (!isProduction) {
// Always read fresh template in development
template = await fs.readFile('./index.html', 'utf-8')
template = await vite.transformIndexHtml(url, template)
render = (await vite.ssrLoadModule('/src/entry-server.ts')).render
} else {
template = templateHtml
render = (await import('./dist/server/entry-server.js')).render
}

const rendered = await render(url)
const html = await transformHtmlTemplate(
rendered.unhead,
template
.replace(`<!--app-head-->`, rendered.head ?? '')
.replace(`<!--app-html-->`, rendered.html ?? '')
)

res.status(200).set({ 'Content-Type': 'text/html' }).send(html)
} catch (e) {
vite?.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})

// Start http server
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`)
})
Loading
Loading