mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat: @nuxt/meta
module for head rendering (#179)
Co-authored-by: Anthony Fu <hi@antfu.me> Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
parent
0bbbddecba
commit
b263b4f930
@ -11,6 +11,7 @@
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"vue/one-component-per-file": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"jsdoc/require-jsdoc": "off",
|
||||
"jsdoc/require-param": "off",
|
||||
"jsdoc/require-returns": "off",
|
||||
|
@ -1 +1,75 @@
|
||||
# Meta Tags
|
||||
|
||||
You can customize the meta tags for your site through several different ways:
|
||||
|
||||
## `useMeta` Composable
|
||||
|
||||
Within your `setup()` function you can call `useMeta` with an object of meta properties with keys corresponding to meta tags: `title`, `base`, `script`, `style`, `meta` and `link`, as well as `htmlAttrs` and `bodyAttrs`. Alternatively, you can pass a function returning the object for reactive metadata.
|
||||
|
||||
For example:
|
||||
```ts
|
||||
import { ref } from 'vue'
|
||||
import { useMeta } from '@nuxt/app'
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
useMeta({
|
||||
bodyAttrs: {
|
||||
class: 'test'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `head` Component Property
|
||||
|
||||
If you would prefer to use the options syntax, you can do exactly the same thing within your component options with an object or function called `head`. For example:
|
||||
|
||||
```js
|
||||
export default {
|
||||
head: {
|
||||
script: [
|
||||
{
|
||||
async: true,
|
||||
src: 'https://myscript.com/script.js'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note that `head()` does not have access to the component instance.**
|
||||
|
||||
## Meta Components
|
||||
|
||||
Nuxt provides `<Title>`, `<Base>`, `<Script>`, `<Style>`, `<Meta>`, `<Link>`, `<Body>` and `<Head>` components so that you can interact directly with your metadata within your component template.
|
||||
|
||||
Because these component names match native HTML elements, it is very important that they are capitalized in the template.
|
||||
|
||||
`<Head>` and `<Body>` can accept nested meta tags (for aesthetic reasons) but this has no effect on _where_ the nested meta tags are rendered in the final HTML.
|
||||
|
||||
For example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
Hello World
|
||||
<Head :lang="dynamic > 50 ? 'en-GB' : 'en-US'">
|
||||
<Title>{{ dynamic }} title</Title>
|
||||
<Meta name="description" :content="`My page's ${dynamic} description`" />
|
||||
<Link rel="preload" href="/test.txt" as="script" />
|
||||
</Head>
|
||||
|
||||
<button class="blue" @click="dynamic = Math.random() * 100">
|
||||
Click me
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({ dynamic: 49 })
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
34
examples/meta/app.vue
Normal file
34
examples/meta/app.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
Hello World
|
||||
|
||||
<Head :lang="'' + dynamic">
|
||||
<Title>{{ dynamic }} title</Title>
|
||||
<Meta name="description" :content="`My page's ${dynamic} description`" />
|
||||
<Link rel="preload" href="/test.txt" as="script" />
|
||||
</Head>
|
||||
|
||||
<button class="blue" @click="dynamic = Math.random() * 100">
|
||||
Clickme
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useMeta } from '@nuxt/app'
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
useMeta({
|
||||
bodyAttrs: {
|
||||
class: 'test'
|
||||
}
|
||||
})
|
||||
return { dynamic: ref(49) }
|
||||
},
|
||||
head: {
|
||||
title: 'Another title'
|
||||
}
|
||||
}
|
||||
</script>
|
4
examples/meta/nuxt.config.ts
Normal file
4
examples/meta/nuxt.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { defineNuxtConfig } from '@nuxt/kit'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
})
|
12
examples/meta/package.json
Normal file
12
examples/meta/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "example-meta",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"nuxt3": "latest"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nu dev",
|
||||
"build": "nu build",
|
||||
"start": "node .output/server"
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ export default defineBuildConfig({
|
||||
{ input: 'src/', name: 'app' }
|
||||
],
|
||||
dependencies: [
|
||||
'@vueuse/head',
|
||||
'ohmyfetch',
|
||||
'vue-router',
|
||||
'vuex5'
|
||||
|
@ -20,7 +20,6 @@
|
||||
"prepack": "unbuild"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/head": "^0.6.0",
|
||||
"hookable": "^4.4.1",
|
||||
"ohmyfetch": "^0.2.0",
|
||||
"upath": "^2.0.1",
|
||||
@ -28,6 +27,9 @@
|
||||
"vue-router": "^4.0.10",
|
||||
"vuex5": "^0.5.0-testing.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/meta": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unbuild": "^0.3.2"
|
||||
}
|
||||
|
@ -1,2 +1,5 @@
|
||||
export * from './nuxt'
|
||||
export * from './composables'
|
||||
|
||||
// @ts-ignore
|
||||
export * from '@nuxt/meta'
|
||||
|
@ -3,6 +3,15 @@ import Hookable from 'hookable'
|
||||
import { defineGetter } from './utils'
|
||||
import { legacyPlugin, LegacyContext } from './legacy'
|
||||
|
||||
type NuxtMeta = {
|
||||
htmlAttrs?: string
|
||||
headAttrs?: string
|
||||
bodyAttrs?: string
|
||||
headTags?: string
|
||||
bodyPrepend?: string
|
||||
bodyScripts?: string
|
||||
}
|
||||
|
||||
export interface Nuxt {
|
||||
app: App
|
||||
globalName: string
|
||||
@ -16,7 +25,9 @@ export interface Nuxt {
|
||||
_asyncDataPromises?: Record<string, Promise<any>>
|
||||
_legacyContext?: LegacyContext
|
||||
|
||||
ssrContext?: Record<string, any>
|
||||
ssrContext?: Record<string, any> & {
|
||||
renderMeta: () => Promise<NuxtMeta> | NuxtMeta
|
||||
}
|
||||
payload: {
|
||||
serverRendered?: true
|
||||
data?: Record<string, any>
|
||||
|
@ -1,56 +0,0 @@
|
||||
import { defineComponent } from '@vue/runtime-core'
|
||||
import { useHead, HeadObject } from '@vueuse/head'
|
||||
|
||||
type MappedProps<T extends Record<string, any>> = {
|
||||
[P in keyof T]: { type: () => T[P] }
|
||||
}
|
||||
|
||||
const props: MappedProps<HeadObject> = {
|
||||
base: { type: Object },
|
||||
bodyAttrs: { type: Object },
|
||||
htmlAttrs: { type: Object },
|
||||
link: { type: Array },
|
||||
meta: { type: Array },
|
||||
script: { type: Array },
|
||||
style: { type: Array },
|
||||
title: { type: String }
|
||||
}
|
||||
|
||||
export const Head = defineComponent({
|
||||
props,
|
||||
setup (props, { slots }) {
|
||||
useHead(() => props)
|
||||
|
||||
return () => slots.default?.()
|
||||
}
|
||||
})
|
||||
|
||||
const createHeadComponent = (prop: keyof typeof props, isArray = false) =>
|
||||
defineComponent({
|
||||
setup (_props, { attrs, slots }) {
|
||||
useHead(() => ({
|
||||
[prop]: isArray ? [attrs] : attrs
|
||||
}))
|
||||
|
||||
return () => slots.default?.()
|
||||
}
|
||||
})
|
||||
|
||||
const createHeadComponentFromSlot = (prop: keyof typeof props) =>
|
||||
defineComponent({
|
||||
setup (_props, { slots }) {
|
||||
useHead(() => ({
|
||||
[prop]: slots.default?.()[0]?.children
|
||||
}))
|
||||
|
||||
return () => null
|
||||
}
|
||||
})
|
||||
|
||||
export const Html = createHeadComponent('htmlAttrs')
|
||||
export const Body = createHeadComponent('bodyAttrs')
|
||||
export const Title = createHeadComponentFromSlot('title')
|
||||
export const Meta = createHeadComponent('meta', true)
|
||||
export const Link = createHeadComponent('link', true)
|
||||
export const Script = createHeadComponent('script', true)
|
||||
export const Style = createHeadComponent('style', true)
|
@ -1,23 +0,0 @@
|
||||
import { createHead, renderHeadToString } from '@vueuse/head'
|
||||
import { defineNuxtPlugin } from '@nuxt/app'
|
||||
import { Head, Html, Body, Title, Meta, Link, Script, Style } from './head'
|
||||
|
||||
export default defineNuxtPlugin((nuxt) => {
|
||||
const { app, ssrContext } = nuxt
|
||||
const head = createHead()
|
||||
|
||||
app.use(head)
|
||||
|
||||
app.component('NuxtHead', Head)
|
||||
app.component('NuxtHtml', Html)
|
||||
app.component('NuxtBody', Body)
|
||||
app.component('NuxtTitle', Title)
|
||||
app.component('NuxtMeta', Meta)
|
||||
app.component('NuxtHeadLink', Link)
|
||||
app.component('NuxtScript', Script)
|
||||
app.component('NuxtStyle', Style)
|
||||
|
||||
if (process.server) {
|
||||
ssrContext.head = () => renderHeadToString(head)
|
||||
}
|
||||
})
|
14
packages/meta/build.config.ts
Normal file
14
packages/meta/build.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { defineBuildConfig } from 'unbuild'
|
||||
|
||||
export default defineBuildConfig({
|
||||
declaration: true,
|
||||
entries: [
|
||||
'src/module',
|
||||
{ input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' }
|
||||
],
|
||||
externals: [
|
||||
'@vue/reactivity',
|
||||
'@vue/shared',
|
||||
'@vueuse/head'
|
||||
]
|
||||
})
|
1
packages/meta/module.js
Normal file
1
packages/meta/module.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/module')
|
34
packages/meta/package.json
Normal file
34
packages/meta/package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@nuxt/meta",
|
||||
"version": "0.1.0",
|
||||
"repository": "nuxt/framework",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
".": "./dist/runtime/index.mjs",
|
||||
"./module": {
|
||||
"import": "./dist/module.mjs",
|
||||
"require": "./dist/module.js"
|
||||
}
|
||||
},
|
||||
"types": "./dist/runtime/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"prepack": "unbuild"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^0.6.4",
|
||||
"@vueuse/head": "^0.6.0",
|
||||
"upath": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unbuild": "^0.3.1",
|
||||
"vue-meta": "3.0.0-alpha.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/reactivity": "3.1.1",
|
||||
"@vue/shared": "3.1.1",
|
||||
"vue": "3.1.1"
|
||||
}
|
||||
}
|
17
packages/meta/src/module.ts
Normal file
17
packages/meta/src/module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { resolve } from 'upath'
|
||||
import { defineNuxtModule } from '@nuxt/kit'
|
||||
|
||||
export default defineNuxtModule({
|
||||
name: 'meta',
|
||||
setup (_options, nuxt) {
|
||||
const runtimeDir = resolve(__dirname, 'runtime')
|
||||
|
||||
nuxt.options.build.transpile.push('@nuxt/meta', runtimeDir)
|
||||
nuxt.options.alias['@nuxt/meta'] = resolve(runtimeDir, 'index')
|
||||
|
||||
nuxt.hook('app:resolve', (app) => {
|
||||
app.plugins.push({ src: resolve(runtimeDir, 'vueuse-head') })
|
||||
app.plugins.push({ src: resolve(runtimeDir, 'meta') })
|
||||
})
|
||||
}
|
||||
})
|
209
packages/meta/src/runtime/components.ts
Normal file
209
packages/meta/src/runtime/components.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import { defineComponent, SetupContext } from 'vue'
|
||||
import { useMeta } from './index'
|
||||
|
||||
type Props = Readonly<Record<string, any>>
|
||||
|
||||
const removeUndefinedProps = (props: Props) =>
|
||||
Object.fromEntries(Object.entries(props).filter(([_key, value]) => value !== undefined))
|
||||
|
||||
const setupForUseMeta = (metaFactory: (props: Props, ctx: SetupContext) => Record<string, any>, renderChild?: boolean) => (props: Props, ctx: SetupContext) => {
|
||||
useMeta(() => metaFactory(removeUndefinedProps(props), ctx))
|
||||
return () => renderChild ? ctx.slots.default?.() : null
|
||||
}
|
||||
|
||||
const globalProps = {
|
||||
accesskey: String,
|
||||
autocapitalize: String,
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
class: String,
|
||||
contenteditable: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
contextmenu: String,
|
||||
dir: String,
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
enterkeyhint: String,
|
||||
exportparts: String,
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
id: String,
|
||||
inputmode: String,
|
||||
is: String,
|
||||
itemid: String,
|
||||
itemprop: String,
|
||||
itemref: String,
|
||||
itemscope: String,
|
||||
itemtype: String,
|
||||
lang: String,
|
||||
nonce: String,
|
||||
part: String,
|
||||
slot: String,
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
style: String,
|
||||
tabindex: String,
|
||||
title: String,
|
||||
translate: String
|
||||
}
|
||||
|
||||
// <script>
|
||||
export const Script = defineComponent({
|
||||
name: 'Script',
|
||||
props: {
|
||||
...globalProps,
|
||||
async: Boolean,
|
||||
crossorigin: {
|
||||
type: [Boolean, String],
|
||||
default: undefined
|
||||
},
|
||||
defer: Boolean,
|
||||
integrity: String,
|
||||
nomodule: Boolean,
|
||||
nonce: String,
|
||||
referrerpolicy: String,
|
||||
src: String,
|
||||
type: String,
|
||||
/** @deprecated **/
|
||||
charset: String,
|
||||
/** @deprecated **/
|
||||
language: String
|
||||
},
|
||||
setup: setupForUseMeta(script => ({
|
||||
script: [script]
|
||||
}))
|
||||
})
|
||||
|
||||
// <link>
|
||||
export const Link = defineComponent({
|
||||
name: 'Link',
|
||||
props: {
|
||||
...globalProps,
|
||||
as: String,
|
||||
crossorigin: String,
|
||||
disabled: Boolean,
|
||||
href: String,
|
||||
hreflang: String,
|
||||
imagesizes: String,
|
||||
imagesrcset: String,
|
||||
integrity: String,
|
||||
media: String,
|
||||
prefetch: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
referrerpolicy: String,
|
||||
rel: String,
|
||||
sizes: String,
|
||||
title: String,
|
||||
type: String,
|
||||
/** @deprecated **/
|
||||
methods: String,
|
||||
/** @deprecated **/
|
||||
target: String
|
||||
},
|
||||
setup: setupForUseMeta(link => ({
|
||||
link: [link]
|
||||
}))
|
||||
})
|
||||
|
||||
// <base>
|
||||
export const Base = defineComponent({
|
||||
name: 'Base',
|
||||
props: {
|
||||
...globalProps,
|
||||
href: String,
|
||||
target: String
|
||||
},
|
||||
setup: setupForUseMeta(base => ({
|
||||
base
|
||||
}))
|
||||
})
|
||||
|
||||
// <title>
|
||||
export const Title = defineComponent({
|
||||
name: 'Title',
|
||||
setup: setupForUseMeta((_, { slots }) => {
|
||||
const title = slots.default()?.[0]?.children || null
|
||||
if (process.dev && title && typeof title !== 'string') {
|
||||
console.error('<Title> can only take a string in its default slot.')
|
||||
}
|
||||
return {
|
||||
title
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// <meta>
|
||||
export const Meta = defineComponent({
|
||||
name: 'Meta',
|
||||
props: {
|
||||
...globalProps,
|
||||
charset: String,
|
||||
content: String,
|
||||
httpEquiv: String,
|
||||
name: String
|
||||
},
|
||||
setup: setupForUseMeta(meta => ({
|
||||
meta: [meta]
|
||||
}))
|
||||
})
|
||||
|
||||
// <style>
|
||||
export const Style = defineComponent({
|
||||
name: 'Style',
|
||||
props: {
|
||||
...globalProps,
|
||||
type: String,
|
||||
media: String,
|
||||
nonce: String,
|
||||
title: String,
|
||||
/** @deprecated **/
|
||||
scoped: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup: setupForUseMeta((props, { slots }) => {
|
||||
const style = { ...props }
|
||||
const textContent = slots.default?.()?.[0]?.children
|
||||
if (textContent) {
|
||||
if (process.dev && typeof textContent !== 'string') {
|
||||
console.error('<Style> can only take a string in its default slot.')
|
||||
}
|
||||
style.content = textContent
|
||||
}
|
||||
return {
|
||||
style: [style]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// <head>
|
||||
export const Head = defineComponent({
|
||||
name: 'Head',
|
||||
props: {
|
||||
...globalProps,
|
||||
manifest: String,
|
||||
version: String,
|
||||
xmlns: String
|
||||
},
|
||||
setup: setupForUseMeta(headAttrs => ({ headAttrs }), true)
|
||||
})
|
||||
|
||||
// <body>
|
||||
export const Body = defineComponent({
|
||||
name: 'Body',
|
||||
props: globalProps,
|
||||
setup: setupForUseMeta(bodyAttrs => ({ bodyAttrs }), true)
|
||||
})
|
25
packages/meta/src/runtime/composables.ts
Normal file
25
packages/meta/src/runtime/composables.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// import { useMeta as useVueMeta } from 'vue-meta'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { computed, ComputedGetter } from '@vue/reactivity'
|
||||
import { useHead } from '@vueuse/head'
|
||||
|
||||
/**
|
||||
* You can pass in a meta object, which has keys corresponding to meta tags:
|
||||
* `title`, `base`, `script`, `style`, `meta` and `link`, as well as `htmlAttrs` and `bodyAttrs`.
|
||||
*
|
||||
* Alternatively, for reactive meta state, you can pass in a function
|
||||
* that returns a meta object.
|
||||
*/
|
||||
export function useMeta (meta: Record<string, any> | ComputedGetter<any>) {
|
||||
// TODO: refine @nuxt/meta API
|
||||
|
||||
// At the moment we force all interaction to happen through passing in
|
||||
// the meta object or function that returns a meta object.
|
||||
const source = isFunction(meta) ? computed(meta) : meta
|
||||
|
||||
// `vue-meta`
|
||||
// useVueMeta(source)
|
||||
|
||||
// `@vueuse/head`
|
||||
useHead(source)
|
||||
}
|
1
packages/meta/src/runtime/index.ts
Normal file
1
packages/meta/src/runtime/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './composables'
|
19
packages/meta/src/runtime/meta.ts
Normal file
19
packages/meta/src/runtime/meta.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import { defineNuxtPlugin } from '@nuxt/app'
|
||||
import * as Components from './components'
|
||||
import { useMeta } from './index'
|
||||
|
||||
export default defineNuxtPlugin((nuxt) => {
|
||||
nuxt.app.mixin({
|
||||
created () {
|
||||
const instance = getCurrentInstance()
|
||||
if (!instance?.type || !('head' in instance.type)) { return }
|
||||
|
||||
useMeta((instance.type as any).head)
|
||||
}
|
||||
})
|
||||
|
||||
for (const name in Components) {
|
||||
nuxt.app.component(name, Components[name])
|
||||
}
|
||||
})
|
35
packages/meta/src/runtime/vue-meta.ts
Normal file
35
packages/meta/src/runtime/vue-meta.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { defineNuxtPlugin } from '@nuxt/app'
|
||||
import { createApp } from 'vue'
|
||||
import { createMetaManager } from 'vue-meta'
|
||||
|
||||
export default defineNuxtPlugin((nuxt) => {
|
||||
const manager = createMetaManager(process.server)
|
||||
|
||||
nuxt.app.use(manager)
|
||||
|
||||
if (process.client) {
|
||||
const teleportTarget = document.createElement('div')
|
||||
teleportTarget.id = 'head-target'
|
||||
document.body.appendChild(teleportTarget)
|
||||
|
||||
createApp({ render: () => manager.render({}) }).mount('#head-target')
|
||||
}
|
||||
|
||||
if (process.server) {
|
||||
nuxt.ssrContext.renderMeta = async () => {
|
||||
const { renderMetaToString } = await import('vue-meta/ssr')
|
||||
nuxt.ssrContext.teleports = nuxt.ssrContext.teleports || {}
|
||||
|
||||
await renderMetaToString(nuxt.app, nuxt.ssrContext)
|
||||
|
||||
return {
|
||||
htmlAttrs: nuxt.ssrContext.teleports.htmlAttrs || '',
|
||||
headAttrs: nuxt.ssrContext.teleports.headAttrs || '',
|
||||
bodyAttrs: nuxt.ssrContext.teleports.bodyAttrs || '',
|
||||
headTags: nuxt.ssrContext.teleports.head || '',
|
||||
bodyPrepend: nuxt.ssrContext.teleports['body-prepend'] || '',
|
||||
bodyScripts: nuxt.ssrContext.teleports.body || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
12
packages/meta/src/runtime/vueuse-head.ts
Normal file
12
packages/meta/src/runtime/vueuse-head.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { createHead, renderHeadToString } from '@vueuse/head'
|
||||
import { defineNuxtPlugin } from '@nuxt/app'
|
||||
|
||||
export default defineNuxtPlugin((nuxt) => {
|
||||
const head = createHead()
|
||||
|
||||
nuxt.app.use(head)
|
||||
|
||||
if (process.server) {
|
||||
nuxt.ssrContext.renderMeta = () => renderHeadToString(head)
|
||||
}
|
||||
})
|
@ -59,6 +59,12 @@ export default function nuxt2CompatModule () {
|
||||
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs')
|
||||
})
|
||||
|
||||
// Nitro server plugin (for vue-meta)
|
||||
this.addPlugin({
|
||||
fileName: 'nitro-compat.server.js',
|
||||
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro-compat.server.js')
|
||||
})
|
||||
|
||||
// Fix module resolution
|
||||
nuxt.hook('webpack:config', (configs) => {
|
||||
for (const config of configs) {
|
||||
|
27
packages/nitro/src/runtime/app/nitro-compat.server.js
Normal file
27
packages/nitro/src/runtime/app/nitro-compat.server.js
Normal file
@ -0,0 +1,27 @@
|
||||
export default ({ ssrContext }) => {
|
||||
ssrContext.renderMeta = () => {
|
||||
const meta = ssrContext.meta.inject({
|
||||
isSSR: ssrContext.nuxt.serverRendered,
|
||||
ln: process.env.NODE_ENV === 'development'
|
||||
})
|
||||
|
||||
return {
|
||||
htmlAttrs: meta.htmlAttrs.text(),
|
||||
headAttrs: meta.headAttrs.text(),
|
||||
headTags:
|
||||
meta.title.text() + meta.base.text() +
|
||||
meta.meta.text() + meta.link.text() +
|
||||
meta.style.text() + meta.script.text() +
|
||||
meta.noscript.text(),
|
||||
bodyAttrs: meta.bodyAttrs.text(),
|
||||
bodyScriptsPrepend:
|
||||
meta.meta.text({ pbody: true }) + meta.link.text({ pbody: true }) +
|
||||
meta.style.text({ pbody: true }) + meta.script.text({ pbody: true }) +
|
||||
meta.noscript.text({ pbody: true }),
|
||||
bodyScripts:
|
||||
meta.meta.text({ body: true }) + meta.link.text({ body: true }) +
|
||||
meta.style.text({ body: true }) + meta.script.text({ body: true }) +
|
||||
meta.noscript.text({ body: true })
|
||||
}
|
||||
}
|
||||
}
|
@ -68,7 +68,7 @@ export async function renderMiddleware (req, res) {
|
||||
data = renderPayload(payload, url)
|
||||
res.setHeader('Content-Type', 'text/javascript;charset=UTF-8')
|
||||
} else {
|
||||
data = renderHTML(payload, rendered, ssrContext)
|
||||
data = await renderHTML(payload, rendered, ssrContext)
|
||||
res.setHeader('Content-Type', 'text/html;charset=UTF-8')
|
||||
}
|
||||
|
||||
@ -77,56 +77,30 @@ export async function renderMiddleware (req, res) {
|
||||
res.end(data, 'utf-8')
|
||||
}
|
||||
|
||||
function renderHTML (payload, rendered, ssrContext) {
|
||||
async function renderHTML (payload, rendered, ssrContext) {
|
||||
const state = `<script>window.__NUXT__=${devalue(payload)}</script>`
|
||||
const _html = rendered.html
|
||||
const html = rendered.html
|
||||
|
||||
const meta = {
|
||||
htmlAttrs: '',
|
||||
bodyAttrs: '',
|
||||
headAttrs: '',
|
||||
headTags: '',
|
||||
bodyTags: '',
|
||||
bodyScriptsPrepend: '',
|
||||
bodyScripts: ''
|
||||
if ('renderMeta' in ssrContext) {
|
||||
rendered.meta = await ssrContext.renderMeta()
|
||||
}
|
||||
|
||||
// @vueuse/head
|
||||
if (typeof ssrContext.head === 'function') {
|
||||
Object.assign(meta, ssrContext.head())
|
||||
}
|
||||
|
||||
// vue-meta
|
||||
if (ssrContext.meta && typeof ssrContext.meta.inject === 'function') {
|
||||
const _meta = ssrContext.meta.inject({
|
||||
isSSR: ssrContext.nuxt.serverRendered,
|
||||
ln: process.env.NODE_ENV === 'development'
|
||||
})
|
||||
meta.htmlAttrs += _meta.htmlAttrs.text()
|
||||
meta.headAttrs += _meta.headAttrs.text()
|
||||
meta.headTags +=
|
||||
_meta.title.text() + _meta.base.text() +
|
||||
_meta.meta.text() + _meta.link.text() +
|
||||
_meta.style.text() + _meta.script.text() +
|
||||
_meta.noscript.text()
|
||||
meta.bodyAttrs += _meta.bodyAttrs.text()
|
||||
meta.bodyScriptsPrepend =
|
||||
_meta.meta.text({ pbody: true }) + _meta.link.text({ pbody: true }) +
|
||||
_meta.style.text({ pbody: true }) + _meta.script.text({ pbody: true }) +
|
||||
_meta.noscript.text({ pbody: true })
|
||||
meta.bodyScripts =
|
||||
_meta.meta.text({ body: true }) + _meta.link.text({ body: true }) +
|
||||
_meta.style.text({ body: true }) + _meta.script.text({ body: true }) +
|
||||
_meta.noscript.text({ body: true })
|
||||
}
|
||||
const {
|
||||
htmlAttrs = '',
|
||||
bodyAttrs = '',
|
||||
headAttrs = '',
|
||||
headTags = '',
|
||||
bodyScriptsPrepend = '',
|
||||
bodyScripts = ''
|
||||
} = rendered.meta || {}
|
||||
|
||||
return htmlTemplate({
|
||||
HTML_ATTRS: meta.htmlAttrs,
|
||||
HEAD_ATTRS: meta.headAttrs,
|
||||
HEAD: meta.headTags +
|
||||
HTML_ATTRS: htmlAttrs,
|
||||
HEAD_ATTRS: headAttrs,
|
||||
HEAD: headTags +
|
||||
rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''),
|
||||
BODY_ATTRS: meta.bodyAttrs,
|
||||
APP: meta.bodyScriptsPrepend + _html + state + rendered.renderScripts() + meta.bodyScripts
|
||||
BODY_ATTRS: bodyAttrs,
|
||||
APP: bodyScriptsPrepend + html + state + rendered.renderScripts() + bodyScripts
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,11 @@
|
||||
"repository": "nuxt/framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"bin": {
|
||||
"nu": "./bin/nuxt.js",
|
||||
"nuxt": "./bin/nuxt.js",
|
||||
"nuxt-cli": "./bin/nuxt.js"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
@ -11,15 +16,11 @@
|
||||
"scripts": {
|
||||
"prepack": "unbuild $@ || true"
|
||||
},
|
||||
"bin": {
|
||||
"nu": "./bin/nuxt.js",
|
||||
"nuxt": "./bin/nuxt.js",
|
||||
"nuxt-cli": "./bin/nuxt.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/app": "^0.5.0",
|
||||
"@nuxt/component-discovery": "^0.2.0",
|
||||
"@nuxt/kit": "^0.6.4",
|
||||
"@nuxt/meta": "^0.1.0",
|
||||
"@nuxt/nitro": "^0.9.1",
|
||||
"@nuxt/pages": "^0.3.0",
|
||||
"@nuxt/vite-builder": "^0.5.0",
|
||||
|
@ -56,6 +56,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
||||
options._majorVersion = 3
|
||||
options.alias.vue = normalize(require.resolve('vue/dist/vue.esm-bundler.js'))
|
||||
options.buildModules.push(normalize(require.resolve('@nuxt/pages/module')))
|
||||
options.buildModules.push(normalize(require.resolve('@nuxt/meta/module')))
|
||||
options.buildModules.push(normalize(require.resolve('@nuxt/component-discovery/module')))
|
||||
|
||||
const nuxt = createNuxt(options)
|
||||
|
39
yarn.lock
39
yarn.lock
@ -1279,7 +1279,6 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@nuxt/app@workspace:packages/app"
|
||||
dependencies:
|
||||
"@vueuse/head": ^0.6.0
|
||||
hookable: ^4.4.1
|
||||
ohmyfetch: ^0.2.0
|
||||
unbuild: ^0.3.2
|
||||
@ -1287,6 +1286,8 @@ __metadata:
|
||||
vue: ^3.1.4
|
||||
vue-router: ^4.0.10
|
||||
vuex5: ^0.5.0-testing.3
|
||||
peerDependencies:
|
||||
"@nuxt/meta": ^0.1.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -1355,6 +1356,22 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@nuxt/meta@^0.1.0, @nuxt/meta@workspace:packages/meta":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@nuxt/meta@workspace:packages/meta"
|
||||
dependencies:
|
||||
"@nuxt/kit": ^0.6.4
|
||||
"@vueuse/head": ^0.6.0
|
||||
unbuild: ^0.3.1
|
||||
upath: ^2.0.1
|
||||
vue-meta: 3.0.0-alpha.9
|
||||
peerDependencies:
|
||||
"@vue/reactivity": 3.1.1
|
||||
"@vue/shared": 3.1.1
|
||||
vue: 3.1.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@nuxt/nitro@^0.9.1, @nuxt/nitro@workspace:packages/nitro":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@nuxt/nitro@workspace:packages/nitro"
|
||||
@ -5416,6 +5433,14 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"example-meta@workspace:examples/meta":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "example-meta@workspace:examples/meta"
|
||||
dependencies:
|
||||
nuxt3: latest
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"example-pages@workspace:examples/pages":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "example-pages@workspace:examples/pages"
|
||||
@ -8621,6 +8646,7 @@ fsevents@~2.3.2:
|
||||
"@nuxt/app": ^0.5.0
|
||||
"@nuxt/component-discovery": ^0.2.0
|
||||
"@nuxt/kit": ^0.6.4
|
||||
"@nuxt/meta": ^0.1.0
|
||||
"@nuxt/nitro": ^0.9.1
|
||||
"@nuxt/pages": ^0.3.0
|
||||
"@nuxt/vite-builder": ^0.5.0
|
||||
@ -11667,7 +11693,7 @@ fsevents@~2.3.2:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unbuild@npm:^0.3.2":
|
||||
"unbuild@npm:^0.3.1, unbuild@npm:^0.3.2":
|
||||
version: 0.3.2
|
||||
resolution: "unbuild@npm:0.3.2"
|
||||
dependencies:
|
||||
@ -11987,6 +12013,15 @@ fsevents@~2.3.2:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vue-meta@npm:3.0.0-alpha.9":
|
||||
version: 3.0.0-alpha.9
|
||||
resolution: "vue-meta@npm:3.0.0-alpha.9"
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
checksum: bc0ccfee2b2fa9bdbf7a382a4af671def09c79ba2bbf3c89e54d642ec2de18cc1ad91f3ecd266e3cc8eb01547cef743648fd63b4392dd1c4b5663c6f7b94351f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vue-router@npm:^4.0.10":
|
||||
version: 4.0.10
|
||||
resolution: "vue-router@npm:4.0.10"
|
||||
|
Loading…
Reference in New Issue
Block a user