diff --git a/README.md b/README.md index 68427c68..63ce7bbb 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ export default defineNuxtConfig({ }) ``` -This `useHead` composable uses `@vueuse/head` under the hood (rather than `vue-meta`) to manipulate your ``. +This `useHead` composable uses `@unhead/vue` under the hood (rather than `vue-meta`) to manipulate your ``. Accordingly, we recommend not to use both the native Nuxt 2 `head()` properties as well as `useHead`, as they may conflict. For more information on how to use this composable, see [the docs](https://nuxt.com/docs/getting-started/seo-meta#seo-and-meta). diff --git a/packages/bridge-schema/build.config.ts b/packages/bridge-schema/build.config.ts index be771c31..e698b5c1 100644 --- a/packages/bridge-schema/build.config.ts +++ b/packages/bridge-schema/build.config.ts @@ -40,7 +40,6 @@ export default defineBuildConfig({ 'vue-meta', 'vue-router', 'vue-bundle-renderer', - '@vueuse/head', 'vue', 'hookable', 'nitropack', @@ -57,6 +56,7 @@ export default defineBuildConfig({ 'postcss', 'consola', 'ignore', + '@unhead/schema', // Implicit '@vue/compiler-core', '@vue/shared', diff --git a/packages/bridge-schema/package.json b/packages/bridge-schema/package.json index de6aea1d..4e7430ee 100644 --- a/packages/bridge-schema/package.json +++ b/packages/bridge-schema/package.json @@ -17,7 +17,7 @@ "devDependencies": { "@types/lodash.template": "^4.5.1", "@types/semver": "^7.5.0", - "@vueuse/head": "^1.1.26", + "@unhead/schema": "^1.1.27", "nitropack": "^2.4.1", "unbuild": "latest", "vite": "~4.3.9" diff --git a/packages/bridge-schema/src/config/app.ts b/packages/bridge-schema/src/config/app.ts index c8c5f5e9..c1de9d10 100644 --- a/packages/bridge-schema/src/config/app.ts +++ b/packages/bridge-schema/src/config/app.ts @@ -2,12 +2,12 @@ import { existsSync, readdirSync } from 'node:fs' import { defineUntypedSchema } from 'untyped' import { resolve, join } from 'pathe' import defu from 'defu' +import { AppHeadMetaObject } from '../types/head' export default defineUntypedSchema({ vue: { /** * Properties that will be set directly on `Vue.config` for vue@2. - * * @see [vue@2 Documentation](https://v2.vuejs.org/v2/api/#Global-Config) * @type {typeof import('vue/types/vue').VueConfiguration} */ @@ -24,18 +24,75 @@ export default defineUntypedSchema({ app: { /** * The folder name for the built site assets, relative to `baseURL` (or `cdnURL` if set). - * * @deprecated - use `buildAssetsDir` instead */ assetsPath: { $resolve: async (val, get) => val ?? (await get('buildAssetsDir')) + }, + /** + * Set default configuration for `` on every page. + * @example + * ```js + * app: { + * head: { + * meta: [ + * // + * { name: 'viewport', content: 'width=device-width, initial-scale=1' } + * ], + * script: [ + * // + * { src: 'https://awesome-lib.js' } + * ], + * link: [ + * // + * { rel: 'stylesheet', href: 'https://awesome-lib.css' } + * ], + * // please note that this is an area that is likely to change + * style: [ + * // + * { children: ':root { color: red }', type: 'text/css' } + * ], + * noscript: [ + * // + * { children: 'JavaScript is required' } + * ] + * } + * } + * ``` + * @type {typeof import('../src/types/head').AppHeadMetaObject} + */ + head: { + $resolve: async (val, get) => { + const resolved: Required = defu(val, await get('meta'), { + meta: [], + link: [], + style: [], + script: [], + noscript: [] + }) + + // provides default charset and viewport if not set + if (!resolved.meta.find(m => m.charset)?.charset) { + resolved.meta.unshift({ charset: resolved.charset || 'utf-8' }) + } + if (!resolved.meta.find(m => m.name === 'viewport')?.content) { + resolved.meta.unshift({ name: 'viewport', content: resolved.viewport || 'width=device-width, initial-scale=1' }) + } + + resolved.meta = resolved.meta.filter(Boolean) + resolved.link = resolved.link.filter(Boolean) + resolved.style = resolved.style.filter(Boolean) + resolved.script = resolved.script.filter(Boolean) + resolved.noscript = resolved.noscript.filter(Boolean) + + return resolved + } } }, /** * The path to an HTML template file for rendering Nuxt responses. * Uses `/app.html` if it exists, or the Nuxt's default template if not. - * * @example * ```html * @@ -75,7 +132,6 @@ export default defineUntypedSchema({ /** * Options to pass directly to `vue-meta`. - * * @see [documentation](https://vue-meta.nuxtjs.org/api/#plugin-options). * @type {typeof import('vue-meta').VueMetaOptions} */ @@ -83,7 +139,6 @@ export default defineUntypedSchema({ /** * Set default configuration for `` on every page. - * * @see [documentation](https://vue-meta.nuxtjs.org/api/#metainfo-properties) for specifics. * @type {typeof import('vue-meta').MetaInfo} */ @@ -123,7 +178,6 @@ export default defineUntypedSchema({ * You may want to extend plugins or change their order. For this, you can pass * a function using `extendPlugins`. It accepts an array of plugin objects and * should return an array of plugin objects. - * * @type {(plugins: Array<{ src: string, mode?: 'client' | 'server' }>) => Array<{ src: string, mode?: 'client' | 'server' }>} */ extendPlugins: null, @@ -132,7 +186,6 @@ export default defineUntypedSchema({ * An object where each key name maps to a path to a layout .vue file. * * Normally, there is no need to configure this directly. - * * @type {Record} */ layouts: {}, @@ -141,7 +194,6 @@ export default defineUntypedSchema({ * Set a custom error page layout. * * Normally, there is no need to configure this directly. - * * @type {string} */ ErrorPage: null, @@ -206,7 +258,6 @@ export default defineUntypedSchema({ * * You can either pass a string (the transition name) or an object with properties to bind * to the `` component that will wrap your pages. - * * @see [vue@2 documentation](https://v2.vuejs.org/v2/guide/transitions.html) * @see [vue@3 documentation](https://vuejs.org/guide/built-ins/transition-group.html#enter-leave-transitions) */ @@ -229,7 +280,6 @@ export default defineUntypedSchema({ * * You can either pass a string (the transition name) or an object with properties to bind * to the `` component that will wrap your layouts. - * * @see [vue@2 documentation](https://v2.vuejs.org/v2/guide/transitions.html) */ layoutTransition: { diff --git a/packages/bridge-schema/src/types/head.ts b/packages/bridge-schema/src/types/head.ts new file mode 100644 index 00000000..70f54cf0 --- /dev/null +++ b/packages/bridge-schema/src/types/head.ts @@ -0,0 +1,31 @@ +import type { Head, MergeHead } from '@unhead/schema' + +/** @deprecated Extend types from `@unhead/schema` directly. This may be removed in a future minor version. */ +export interface HeadAugmentations extends MergeHead { + // runtime type modifications + base?: {} + link?: {} + meta?: {} + style?: {} + script?: {} + noscript?: {} + htmlAttrs?: {} + bodyAttrs?: {} +} + +export type MetaObjectRaw = Head +export type MetaObject = MetaObjectRaw + +export type AppHeadMetaObject = MetaObjectRaw & { + /** + * The character encoding in which the document is encoded => `` + * @default `'utf-8'` + */ + charset?: string + /** + * Configuration of the viewport (the area of the window in which web content can be seen), + * mapped to => `` + * @default `'width=device-width, initial-scale=1'` + */ + viewport?: string +} diff --git a/packages/bridge/build.config.ts b/packages/bridge/build.config.ts index 8943b9ea..a7ead832 100644 --- a/packages/bridge/build.config.ts +++ b/packages/bridge/build.config.ts @@ -10,6 +10,7 @@ export default defineBuildConfig({ 'webpack', 'vite', 'vue', - 'vue-meta' + 'vue-meta', + '@unhead/vue' ] }) diff --git a/packages/bridge/package.json b/packages/bridge/package.json index f2c42b6b..c0e652fc 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -29,6 +29,8 @@ "@nuxt/postcss8": "^1.1.3", "@nuxt/schema": "3.5.3", "@nuxt/ui-templates": "^1.2.0", + "@unhead/ssr": "^1.1.27", + "@unhead/vue": "^1.1.27", "@vitejs/plugin-legacy": "^4.0.4", "@vitejs/plugin-vue2": "^2.2.0", "acorn": "^8.9.0", @@ -79,7 +81,6 @@ "@types/fs-extra": "^9.0.13", "@types/hash-sum": "^1.0.0", "@types/node-fetch": "^3.0.2", - "@vueuse/head": "^1.1.26", "nuxt": "^2.17.0", "unbuild": "1.2.1", "vue": "^2.7.14", diff --git a/packages/bridge/src/head.ts b/packages/bridge/src/head.ts index 4c601342..04a36ddb 100644 --- a/packages/bridge/src/head.ts +++ b/packages/bridge/src/head.ts @@ -1,9 +1,11 @@ import { resolve } from 'pathe' -import { addPlugin, addTemplate, defineNuxtModule, tryResolveModule } from '@nuxt/kit' +import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule } from '@nuxt/kit' import { defu } from 'defu' import type { MetaObject } from '@nuxt/schema' import { distDir } from './dirs' +const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head', 'Html', 'Body'] + export default defineNuxtModule({ meta: { name: 'meta' @@ -15,13 +17,26 @@ export default defineNuxtModule({ setup (options, nuxt) { const runtimeDir = nuxt.options.alias['#head'] || resolve(distDir, 'head/runtime') - // Transpile @nuxt/meta and @vueuse/head - nuxt.options.build.transpile.push('@vueuse/head') + // Transpile @unhead/vue and @unhead/ssr nuxt.options.build.transpile.push('unhead') // Add #head alias nuxt.options.alias['#head'] = runtimeDir + // Register components + const componentsPath = resolve(runtimeDir, 'components') + for (const componentName of components) { + addComponent({ + name: componentName, + filePath: componentsPath, + export: componentName, + // built-in that we do not expect the user to override + priority: 10, + // kebab case version of these tags is not valid + kebabName: componentName + }) + } + // Global meta -for Bridge, this is necessary to repeat here // and in packages/schema/src/config/_app.ts const globalMeta: MetaObject = defu(nuxt.options.app.head, { @@ -35,14 +50,24 @@ export default defineNuxtModule({ getContents: () => 'export default ' + JSON.stringify({ globalMeta, mixinKey: 'setup' }) }) - if (!tryResolveModule('@vueuse/head')) { - console.warn('[bridge] Could not find `@vueuse/head`. You may need to install it.') - } + addImportsSources({ + from: '@unhead/vue', + // hard-coded for now we so don't support auto-imports on the deprecated composables + imports: [ + 'injectHead', + 'useHead', + 'useSeoMeta', + 'useHeadSafe', + 'useServerHead', + 'useServerSeoMeta', + 'useServerHeadSafe' + ] + }) // Add generic plugin addPlugin({ src: resolve(runtimeDir, 'plugin') }) - // Add library specific plugin - addPlugin({ src: resolve(runtimeDir, 'vueuse-head.plugin') }) + // Add library-specific plugin + addPlugin({ src: resolve(runtimeDir, 'plugins/unhead') }) } }) diff --git a/packages/bridge/src/imports/presets.ts b/packages/bridge/src/imports/presets.ts index 5bac6869..0b453136 100644 --- a/packages/bridge/src/imports/presets.ts +++ b/packages/bridge/src/imports/presets.ts @@ -5,7 +5,6 @@ export const commonPresets: InlinePreset[] = [ defineUnimportPreset({ from: '#head', imports: [ - 'useHead', 'useMeta' ] }), diff --git a/packages/bridge/src/runtime/app.plugin.mjs b/packages/bridge/src/runtime/app.plugin.mjs index 40bc0350..43bfe915 100644 --- a/packages/bridge/src/runtime/app.plugin.mjs +++ b/packages/bridge/src/runtime/app.plugin.mjs @@ -41,7 +41,7 @@ export default async (ctx, inject) => { provide: inject, unmount: () => { }, use (vuePlugin) { - runOnceWith(vuePlugin, () => vuePlugin.install(this)) + runOnceWith(vuePlugin, () => Vue.use(vuePlugin)) }, version }, diff --git a/packages/bridge/src/runtime/head/components.ts b/packages/bridge/src/runtime/head/components.ts index 651b77fb..72e30096 100644 --- a/packages/bridge/src/runtime/head/components.ts +++ b/packages/bridge/src/runtime/head/components.ts @@ -1,6 +1,6 @@ import { defineComponent } from 'vue' import type { SetupContext } from 'vue' -import { useHead } from './composables' +import { useHead } from '@unhead/vue' type Props = Readonly> @@ -58,10 +58,36 @@ const globalProps = { translate: String } +//