mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-18 04:00:34 +03:00
Merge branch 'next'
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["istanbul"]
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
-20
@@ -1,38 +1,129 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node
|
||||
steps:
|
||||
# Checkout repository
|
||||
- checkout
|
||||
version: 2.1
|
||||
|
||||
# Restore cache
|
||||
executors:
|
||||
node:
|
||||
parameters:
|
||||
browsers:
|
||||
type: boolean
|
||||
default: false
|
||||
docker:
|
||||
- image: circleci/node:latest<<# parameters.browsers >>-browsers<</ parameters.browsers >>
|
||||
working_directory: ~/project
|
||||
environment:
|
||||
NODE_ENV: test
|
||||
|
||||
commands:
|
||||
attach-project:
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
executor: node
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: yarn-{{ checksum "yarn.lock" }}
|
||||
|
||||
# Install dependencies
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: NODE_ENV=dev yarn
|
||||
|
||||
# Keep cache
|
||||
- save_cache:
|
||||
key: yarn-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- "node_modules"
|
||||
- persist_to_workspace:
|
||||
root: ~/project
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
# Lint
|
||||
lint:
|
||||
executor: node
|
||||
steps:
|
||||
- attach-project
|
||||
- run:
|
||||
name: Lint
|
||||
command: yarn lint
|
||||
|
||||
# Test
|
||||
audit:
|
||||
executor: node
|
||||
steps:
|
||||
- attach-project
|
||||
- run:
|
||||
name: Test
|
||||
command: yarn test
|
||||
name: Security Audit
|
||||
command: yarn audit
|
||||
|
||||
# Coverage
|
||||
test-unit:
|
||||
executor: node
|
||||
steps:
|
||||
- attach-project
|
||||
- run:
|
||||
name: Coverage
|
||||
command: yarn codecov
|
||||
name: Unit Tests
|
||||
command: yarn test:unit --coverage && yarn coverage
|
||||
|
||||
test-e2e-ssr:
|
||||
executor: node
|
||||
steps:
|
||||
- attach-project
|
||||
- run:
|
||||
name: E2E SSR Tests
|
||||
command: yarn test:e2e-ssr
|
||||
- persist_to_workspace:
|
||||
root: ~/project
|
||||
paths:
|
||||
- test/fixtures
|
||||
|
||||
test-e2e-browser:
|
||||
parameters:
|
||||
browserString:
|
||||
type: string
|
||||
executor:
|
||||
name: node
|
||||
browsers: true
|
||||
steps:
|
||||
- attach-project
|
||||
- run:
|
||||
name: E2E Browser Tests
|
||||
command: yarn test:e2e-browser
|
||||
environment:
|
||||
BROWSER_STRING: << parameters.browserString >>
|
||||
|
||||
workflows:
|
||||
version : 2
|
||||
|
||||
commit:
|
||||
jobs:
|
||||
- setup
|
||||
- lint: { requires: [setup] }
|
||||
- audit: { requires: [setup] }
|
||||
- test-unit: { requires: [lint] }
|
||||
- test-e2e-ssr: { requires: [lint] }
|
||||
- test-e2e-browser:
|
||||
name: test-e2e-firefox
|
||||
browserString: firefox/headless
|
||||
requires: [test-e2e-ssr]
|
||||
- test-e2e-browser:
|
||||
name: test-e2e-chrome
|
||||
browserString: chrome/selenium
|
||||
requires: [test-e2e-ssr]
|
||||
- test-e2e-browser:
|
||||
name: test-e2e-ie
|
||||
browserString: browserstack/local/windows 7/ie:9
|
||||
requires: [test-e2e-ssr]
|
||||
filters:
|
||||
branches: { ignore: /^pull\/.*/ }
|
||||
- test-e2e-browser:
|
||||
name: test-e2e-edge
|
||||
browserString: browserstack/local/edge:15
|
||||
requires: [test-e2e-ssr]
|
||||
filters:
|
||||
branches: { ignore: /^pull\/.*/ }
|
||||
- test-e2e-browser:
|
||||
name: test-e2e-safari
|
||||
browserString: browserstack/local/os x=snow leopard/safari:5.1
|
||||
requires: [test-e2e-ssr]
|
||||
filters:
|
||||
branches: { ignore: /^pull\/.*/ }
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"@nuxtjs"
|
||||
],
|
||||
"globals": {
|
||||
"Vue": "readable"
|
||||
}
|
||||
}
|
||||
+10
@@ -1,6 +1,7 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.err
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
@@ -32,9 +33,18 @@ jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
package-lock.json
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# built code
|
||||
lib
|
||||
es
|
||||
.vue-meta
|
||||
|
||||
# examples yarn lock
|
||||
examples/yarn.lock
|
||||
|
||||
# env vars
|
||||
.env*
|
||||
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2018 Declan de Wet & Sébastien Chopin
|
||||
Copyright (c) 2016-2019
|
||||
- Declan de Wet
|
||||
- Sébastien Chopin
|
||||
- All the amazing contributors (https://github.com/nuxt/vue-meta/graphs/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
<p align="center">
|
||||
<img src="http://imgur.com/258WtHI.png" alt="vue-meta">
|
||||
<img src="./docs/.vuepress/public/vue-meta.png" alt="vue-meta" />
|
||||
</p>
|
||||
|
||||
<h5 align="center">
|
||||
Manage page meta info in Vue 2.0 components. SSR + Streaming supported. Inspired by <a href="https://github.com/nfl/react-helmet">react-helmet</a>.
|
||||
Manage page metadata in Vue.js components with SSR support
|
||||
</h5>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/feross/standard">
|
||||
<img src="https://cdn.rawgit.com/feross/standard/master/badge.svg" alt="Standard - JavaScript Style">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nuxt/vue-meta/releases/latest"><img src="https://img.shields.io/github/release/nuxt/vue-meta.svg" alt="github release"></a> <a href="http://npmjs.org/package/vue-meta"><img src="https://img.shields.io/npm/v/vue-meta.svg" alt="npm version"></a> <a href="https://circleci.com/gh/nuxt/vue-meta/"><img src="https://badgen.net/circleci/github/nuxt/vue-meta" alt="Build Status"></a> <a href="https://codecov.io/gh/nuxt/vue-meta"><img src="https://codecov.io/gh/nuxt/vue-meta/branch/master/graph/badge.svg" alt="codecov"></a><br>
|
||||
<a href="https://david-dm.org/nuxt/vue-meta"><img src="https://david-dm.org/nuxt/vue-meta/status.svg" alt="dependencies Status"></a> <a href="https://david-dm.org/nuxt/vue-meta?type=dev"><img src="https://david-dm.org/nuxt/vue-meta/dev-status.svg" alt="devDependencies Status"></a><br>
|
||||
<a href="http://npm-stat.com/charts.html?package=vue-meta"><img src="https://img.shields.io/npm/dm/vue-meta.svg" alt="npm downloads"></a> <a href="https://gitter.im/nuxt/vue-meta"><img src="https://badges.gitter.im/nuxt/vue-meta.svg" alt="Gitter"></a>
|
||||
<a href="http://npm-stat.com/charts.html?package=vue-meta"><img src="https://img.shields.io/npm/dm/vue-meta.svg" alt="npm downloads"></a>
|
||||
<a href="http://npmjs.org/package/vue-meta"><img src="https://img.shields.io/npm/v/vue-meta.svg" alt="npm version"></a>
|
||||
<a href="https://codecov.io/gh/nuxt/vue-meta"><img src="https://badgen.net/codecov/c/github/nuxt/vue-meta/master" alt="Coverage Status"></a>
|
||||
<a href="https://circleci.com/gh/nuxt/vue-meta/"><img src="https://badgen.net/circleci/github/nuxt/vue-meta" alt="Build Status"></a>
|
||||
<a href="https://david-dm.org/nuxt/vue-meta"><img src="https://david-dm.org/nuxt/vue-meta/status.svg" alt="dependencies Status"></a>
|
||||
<a href="https://discord.nuxtjs.org/"><img src="https://badgen.net/badge/Discord/join-us/7289DA" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||
```html
|
||||
@@ -26,825 +23,87 @@
|
||||
<script>
|
||||
export default {
|
||||
metaInfo: {
|
||||
title: 'My Example App', // set a title
|
||||
titleTemplate: '%s - Yay!', // title is now "My Example App - Yay!"
|
||||
title: 'My Example App',
|
||||
titleTemplate: '%s - Yay!',
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
amp: undefined // "amp" has no value
|
||||
amp: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
```html
|
||||
<html lang="en" amp>
|
||||
<head>
|
||||
<title>My Example App - Yay!</title>
|
||||
...
|
||||
</head>
|
||||
```
|
||||
# Introduction
|
||||
Vue Meta is a [Vue.js](https://vuejs.org) plugin that allows you to manage your app's metadata. It is inspired by and works similar as [`react-helmet`](https://github.com/nfl/react-helmet) for react. However, instead of setting your data as props passed to a proprietary component, you simply export it as part of your component's data using the `metaInfo` property.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# Table of Contents
|
||||
These properties, when set on a deeply nested component, will cleverly overwrite their parent components' `metaInfo`, thereby enabling custom info for each top-level view as well as coupling metadata directly to deeply nested subcomponents for more maintainable code.
|
||||
|
||||
- [Description](#description)
|
||||
- [Installation](#installation)
|
||||
- [Yarn](#yarn)
|
||||
- [NPM](#npm)
|
||||
- [CDN](#cdn)
|
||||
- [Usage](#usage)
|
||||
- [Step 1: Preparing the plugin](#step-1-preparing-the-plugin)
|
||||
- [Options](#options)
|
||||
- [Step 2: Server Rendering (Optional)](#step-2-server-rendering-optional)
|
||||
- [Step 2.1: Exposing `$meta` to `bundleRenderer`](#step-21-exposing-meta-to-bundlerenderer)
|
||||
- [Step 2.2: Populating the document meta info with `inject()`](#step-22-populating-the-document-meta-info-with-inject)
|
||||
- [Simple Rendering with `renderToString()`](#simple-rendering-with-rendertostring)
|
||||
- [Streaming Rendering with `renderToStream()`](#streaming-rendering-with-rendertostream)
|
||||
- [Step 3: Start defining `metaInfo`](#step-3-start-defining-metainfo)
|
||||
- [Recognized `metaInfo` Properties](#recognized-metainfo-properties)
|
||||
- [`title` (String)](#title-string)
|
||||
- [`titleTemplate` (String | Function)](#titletemplate-string--function)
|
||||
- [`htmlAttrs` (Object)](#htmlattrs-object)
|
||||
- [`headAttrs` (Object)](#headattrs-object)
|
||||
- [`bodyAttrs` (Object)](#bodyattrs-object)
|
||||
- [`base` (Object)](#base-object)
|
||||
- [`meta` ([Object])](#meta-object)
|
||||
- [`link` ([Object])](#link-object)
|
||||
- [`style` ([Object])](#style-object)
|
||||
- [`script` ([Object])](#script-object)
|
||||
- [`noscript` ([Object])](#noscript-object)
|
||||
- [`__dangerouslyDisableSanitizers` ([String])](#__dangerouslydisablesanitizers-string)
|
||||
- [`__dangerouslyDisableSanitizersByTagID` ({[String]})](#__dangerouslydisablesanitizersbytagid-string)
|
||||
- [`changed` (Function)](#changed-function)
|
||||
- [How `metaInfo` is Resolved](#how-metainfo-is-resolved)
|
||||
- [Lists of Tags](#lists-of-tags)
|
||||
- [Performance](#performance)
|
||||
- [How to prevent the update on the initial page render](#how-to-prevent-the-update-on-the-initial-page-render)
|
||||
- [FAQ](#faq)
|
||||
- [How do I use component props and/or component data in `metaInfo`?](#how-do-i-use-component-props-andor-component-data-in-metainfo)
|
||||
- [How do I populate `metaInfo` from the result of an asynchronous action?](#how-do-i-populate-metainfo-from-the-result-of-an-asynchronous-action)
|
||||
- [Why doesn't `vue-meta` support `jsnext:main`?](#why-doesnt-vue-meta-support-jsnextmain)
|
||||
- [Examples](#examples)
|
||||
## Documentation
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
Please find the documention on https://vue-meta.nuxtjs.org
|
||||
|
||||
> :globe_with_meridians: Please help us translate the documentation into your language
|
||||
|
||||
# Description
|
||||
`vue-meta` is a [Vue 2.0](https://vuejs.org) plugin that allows you to manage your app's meta information, much like [`react-helmet`](https://github.com/nfl/react-helmet) does for React. However, instead of setting your data as props passed to a proprietary component, you simply export it as part of your component's data using the `metaInfo` property.
|
||||
## Examples
|
||||
|
||||
These properties, when set on a deeply nested component, will cleverly overwrite their parent components' `metaInfo`, thereby enabling custom info for each top-level view as well as coupling meta info directly to deeply nested subcomponents for more maintainable code.
|
||||
Looking for more examples what vue-meta can do for you? Have a look at the [examples](https://github.com/nuxt/vue-meta/tree/master/examples)
|
||||
|
||||
# Installation
|
||||
## Installation
|
||||
|
||||
### Yarn
|
||||
##### Yarn
|
||||
```sh
|
||||
$ yarn add vue-meta
|
||||
```
|
||||
|
||||
### NPM
|
||||
##### npm
|
||||
```sh
|
||||
$ npm install vue-meta --save
|
||||
```
|
||||
|
||||
### CDN
|
||||
##### Download / CDN
|
||||
|
||||
Use the links below - if you want a previous version, check the instructions at https://unpkg.com.
|
||||
Use the download links below - if you want a previous version, check the instructions at https://unpkg.com.
|
||||
|
||||
Latest version: https://unpkg.com/vue-meta/lib/vue-meta.min.js
|
||||
|
||||
Latest v1.x version: https://unpkg.com/vue-meta@1/lib/vue-meta.min.js
|
||||
|
||||
<!-- start CDN generator - do **NOT** remove this comment -->
|
||||
**Uncompressed:**
|
||||
```html
|
||||
<script src="https://unpkg.com/vue-meta@1.6.0/lib/vue-meta.js"></script>
|
||||
<script src="https://unpkg.com/vue-meta/lib/vue-meta.js"></script>
|
||||
```
|
||||
|
||||
**Minified:**
|
||||
```html
|
||||
<script src="https://unpkg.com/vue-meta@1.6.0/lib/vue-meta.min.js"></script>
|
||||
<script src="https://unpkg.com/vue-meta/lib/vue-meta.min.js"></script>
|
||||
```
|
||||
<!-- end CDN generator - do **NOT** remove this comment -->
|
||||
|
||||
# Usage
|
||||
## Quick Usage
|
||||
|
||||
## Step 1: Preparing the plugin
|
||||
> This step is optional if you don't need SSR and `Vue` is available as a global variable. `vue-meta` will install itself in this case.
|
||||
|
||||
In order to use this plugin, you first need to pass it to `Vue.use` - if you're not rendering on the server-side, your JS entry file will suffice. If you are rendering on the server, then place it in a file that runs both on the server and on the client before your root instance is mounted. If you're using [`vue-router`](https://github.com/vuejs/vue-router), then your main `router.js` file is a good place:
|
||||
|
||||
**router.js:**
|
||||
See the [documentation](https://vue-meta.nuxtjs.org) for more information
|
||||
```js
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Meta from 'vue-meta'
|
||||
import VueMeta from 'vue-meta'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(Meta)
|
||||
|
||||
export default new Router({
|
||||
...
|
||||
Vue.use(VueMeta, {
|
||||
// optional pluginOptions
|
||||
refreshOnceOnNavigation: true
|
||||
})
|
||||
```
|
||||
|
||||
#### Options
|
||||
## Higher level frameworks using vue-meta
|
||||
If you wish to create your app even more quickly, take a look at the following frameworks which use vue-meta
|
||||
|
||||
`vue-meta` allows a few custom options:
|
||||
- [Nuxt.js](https://github.com/nuxt/nuxt.js) - The Vue.js Meta framework
|
||||
- [Gridsome](https://github.com/gridsome/gridsome) - The Vue.js JAMstack framework
|
||||
|
||||
```js
|
||||
Vue.use(Meta, {
|
||||
keyName: 'metaInfo', // the component option name that vue-meta looks for meta info on.
|
||||
attribute: 'data-vue-meta', // the attribute name vue-meta adds to the tags it observes
|
||||
ssrAttribute: 'data-vue-meta-server-rendered', // the attribute name that lets vue-meta know that meta info has already been server-rendered
|
||||
tagIDKeyName: 'vmid' // the property name that vue-meta uses to determine whether to overwrite or append a tag
|
||||
})
|
||||
```
|
||||
# License
|
||||
|
||||
If you don't care about server-side rendering, you can skip straight to [step 3](#step-3-start-defining-metainfo). Otherwise, continue. :smile:
|
||||
|
||||
## Step 2: Server Rendering (Optional)
|
||||
|
||||
If you have an isomorphic/universal webapp, you'll likely want to render your metadata on the server side as well. Here's how.
|
||||
|
||||
### Step 2.1: Exposing `$meta` to `bundleRenderer`
|
||||
|
||||
You'll need to expose the results of the `$meta` method that `vue-meta` adds to the Vue instance to the bundle render context before you can begin injecting your meta information. You'll need to do this in your server entry file:
|
||||
|
||||
**server-entry.js:**
|
||||
```js
|
||||
import app from './app'
|
||||
|
||||
const router = app.$router
|
||||
const meta = app.$meta() // here
|
||||
|
||||
export default (context) => {
|
||||
router.push(context.url)
|
||||
context.meta = meta // and here
|
||||
return app
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2.2: Populating the document meta info with `inject()`
|
||||
|
||||
All that's left for you to do now before you can begin using `metaInfo` options in your components is to make sure they work on the server by `inject`-ing them so you can call `text()` on each item to render out the necessary info. You have two methods at your disposal:
|
||||
|
||||
#### Simple Rendering with `renderToString()`
|
||||
|
||||
Considerably the easiest method to wrap your head around is if your Vue server markup is rendered out as a string:
|
||||
|
||||
**server.js:**
|
||||
|
||||
```js
|
||||
app.get('*', (req, res) => {
|
||||
const context = { url: req.url }
|
||||
renderer.renderToString(context, (error, html) => {
|
||||
if (error) return res.send(error.stack)
|
||||
const bodyOpt = { body: true }
|
||||
const {
|
||||
title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta
|
||||
} = context.meta.inject()
|
||||
return res.send(`
|
||||
<!doctype html>
|
||||
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
|
||||
<head ${headAttrs.text()}>
|
||||
${meta.text()}
|
||||
${title.text()}
|
||||
${link.text()}
|
||||
${style.text()}
|
||||
${script.text()}
|
||||
${noscript.text()}
|
||||
</head>
|
||||
<body ${bodyAttrs.text()}>
|
||||
${html}
|
||||
<script src="/assets/vendor.bundle.js"></script>
|
||||
<script src="/assets/client.bundle.js"></script>
|
||||
${script.text(bodyOpt)}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
If you are using a separate template file, edit your head tag with
|
||||
|
||||
```html
|
||||
<head>
|
||||
{{{ meta.inject().title.text() }}}
|
||||
{{{ meta.inject().meta.text() }}}
|
||||
</head>
|
||||
```
|
||||
|
||||
Notice the use of `{{{` to avoid double escaping. Be extremely cautious when you use `{{{` with `__dangerouslyDisableSanitizers`.
|
||||
|
||||
#### Streaming Rendering with `renderToStream()`
|
||||
|
||||
A little more complex, but well worth it, is to instead stream your response. `vue-meta` supports streaming with no effort (on it's part :stuck_out_tongue_winking_eye:) thanks to Vue's clever `bundleRenderer` context injection:
|
||||
|
||||
**server.js**
|
||||
```js
|
||||
app.get('*', (req, res) => {
|
||||
const context = { url: req.url }
|
||||
const renderStream = renderer.renderToStream(context)
|
||||
renderStream.once('data', () => {
|
||||
const bodyOpt = { body: true }
|
||||
const {
|
||||
title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta
|
||||
} = context.meta.inject()
|
||||
res.write(`
|
||||
<!doctype html>
|
||||
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
|
||||
<head ${headAttrs.text()}>
|
||||
${meta.text()}
|
||||
${title.text()}
|
||||
${link.text()}
|
||||
${style.text()}
|
||||
${script.text()}
|
||||
${noscript.text()}
|
||||
</head>
|
||||
<body ${bodyAttrs.text()}>
|
||||
`)
|
||||
})
|
||||
renderStream.on('data', (chunk) => {
|
||||
res.write(chunk)
|
||||
})
|
||||
renderStream.on('end', () => {
|
||||
res.end(`
|
||||
<script src="/assets/vendor.bundle.js"></script>
|
||||
<script src="/assets/client.bundle.js"></script>
|
||||
${script.text(bodyOpt)}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
renderStream.on('error', (error) => res.status(500).end(`<pre>${error.stack}</pre>`))
|
||||
})
|
||||
```
|
||||
|
||||
## Step 3: Start defining `metaInfo`
|
||||
|
||||
In any of your components, define a `metaInfo` property:
|
||||
|
||||
**App.vue:**
|
||||
```html
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
metaInfo: {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: 'Default Title',
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | My Awesome Webapp'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Home.vue**
|
||||
```html
|
||||
<template>
|
||||
<div id="page">
|
||||
<h1>Home Page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home',
|
||||
metaInfo: {
|
||||
title: 'My Awesome Webapp',
|
||||
// override the parent template and just use the above title only
|
||||
titleTemplate: null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**About.vue**
|
||||
```html
|
||||
<template>
|
||||
<div id="page">
|
||||
<h1>About Page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'About',
|
||||
metaInfo: {
|
||||
// title will be injected into parent titleTemplate
|
||||
title: 'About Us'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Recognized `metaInfo` Properties
|
||||
|
||||
#### `title` (String)
|
||||
|
||||
Maps to the inner-text value of the `<title>` element.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: 'Foo Bar'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title>Foo Bar</title>
|
||||
```
|
||||
|
||||
#### `titleTemplate` (String | Function)
|
||||
|
||||
The value of `title` will be injected into the `%s` placeholder in `titleTemplate` before being rendered. The original title will be available on `metaInfo.titleChunk`.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: 'Foo Bar',
|
||||
titleTemplate: '%s - Baz'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title>Foo Bar - Baz</title>
|
||||
```
|
||||
|
||||
The property can also be a function (from [v1.2.0](https://github.com/nuxt/vue-meta/releases/tag/v1.2.0)):
|
||||
|
||||
```js
|
||||
titleTemplate: (titleChunk) => {
|
||||
// If undefined or blank then we don't need the hyphen
|
||||
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
|
||||
}
|
||||
```
|
||||
|
||||
#### `htmlAttrs` (Object)
|
||||
|
||||
Each **key:value** maps to the equivalent **attribute:value** of the `<html>` element.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
htmlAttrs: {
|
||||
foo: 'bar',
|
||||
amp: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<html foo="bar" amp></html>
|
||||
```
|
||||
|
||||
#### `headAttrs` (Object)
|
||||
|
||||
Each **key:value** maps to the equivalent **attribute:value** of the `<head>` element.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
headAttrs: {
|
||||
foo: 'bar'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<head foo="bar"></head>
|
||||
```
|
||||
|
||||
#### `bodyAttrs` (Object)
|
||||
|
||||
Each **key:value** maps to the equivalent **attribute:value** of the `<body>` element.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
bodyAttrs: {
|
||||
bar: 'baz'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<body bar="baz">Foo Bar</body>
|
||||
```
|
||||
|
||||
#### `base` (Object)
|
||||
|
||||
Maps to a newly-created `<base>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
base: { target: '_blank', href: '/' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<base target="_blank" href="/">
|
||||
```
|
||||
|
||||
#### `meta` ([Object])
|
||||
|
||||
Each item in the array maps to a newly-created `<meta>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
Since v1.5.0, you can now set up meta templates that work similar to the titleTemplate:
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{
|
||||
'property': 'og:title',
|
||||
'content': 'Test title',
|
||||
'template': chunk => `${chunk} - My page`, //or as string template: '%s - My page',
|
||||
'vmid': 'og:title'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta name="og:title" property="og:title" content="Test title - My page">
|
||||
```
|
||||
|
||||
|
||||
#### `link` ([Object])
|
||||
|
||||
Each item in the array maps to a newly-created `<link>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: '/css/index.css' },
|
||||
{ rel: 'favicon', href: 'favicon.ico' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/css/index.css">
|
||||
<link rel="favicon" href="favicon.ico">
|
||||
```
|
||||
|
||||
#### `style` ([Object])
|
||||
|
||||
Each item in the array maps to a newly-created `<style>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
style: [
|
||||
{ cssText: '.foo { color: red }', type: 'text/css' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<style type="text/css">.foo { color: red }</style>
|
||||
```
|
||||
|
||||
#### `script` ([Object])
|
||||
|
||||
Each item in the array maps to a newly-created `<script>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
script: [
|
||||
{ src: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.js', async: true, defer: true }
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" async="true" defer="true"></script>
|
||||
```
|
||||
|
||||
:warning: You have to disable sanitizers so the content of `innerHTML` won't be escaped. Please refer to [\__dangerouslyDisableSanitizers](#__dangerouslydisablesanitizers-string) section below for more info on related risks.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
script: [
|
||||
{ innerHTML: '{ "@context": "http://schema.org" }', type: 'application/ld+json' }
|
||||
],
|
||||
__dangerouslyDisableSanitizers: ['script'],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">{ "@context": "http://schema.org" }</script>
|
||||
```
|
||||
|
||||
If your browser doesn't support `defer` or any other reason, you want to put `<script>` before `</body>`, use `body`.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
script: [
|
||||
{ innerHTML: 'console.log("I am in body");', type: 'text/javascript', body: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `noscript` ([Object])
|
||||
|
||||
Each item in the array maps to a newly-created `<noscript>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
noscript: [
|
||||
{ innerHTML: 'This website requires JavaScript.' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<noscript>This website requires JavaScript.</noscript>
|
||||
```
|
||||
|
||||
#### `__dangerouslyDisableSanitizers` ([String])
|
||||
|
||||
By default, `vue-meta` sanitizes HTML entities in _every_ property. You can disable this behaviour on a per-property basis using `__dangerouslyDisableSantizers`. Just pass it a list of properties you want sanitization to be disabled on:
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: '<I will be sanitized>',
|
||||
meta: [{ vmid: 'description', name: 'description', content: '& I will not be <sanitized>'}],
|
||||
__dangerouslyDisableSanitizers: ['meta']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title><I will be sanitized></title>
|
||||
<meta vmid="description" name="description" content="& I will not be <sanitized>">
|
||||
```
|
||||
|
||||
:warning: **Using this option is not recommended unless you know exactly what you are doing.** By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
|
||||
|
||||
#### `__dangerouslyDisableSanitizersByTagID` ({[String]})
|
||||
|
||||
Provides same functionality as `__dangerouslyDisableSanitizers` but you can specify which property for which `tagIDKeyName`'s sanitization should be disabled. It expects an object with the vmid's as key and an array with property names value:
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: '<I will be sanitized>',
|
||||
meta: [{ vmid: 'description', name: 'still-&-sanitized', content: '& I will not be <sanitized>'}],
|
||||
__dangerouslyDisableSanitizersByTagID: { description: ['content'] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title><I will be sanitized></title>
|
||||
<meta vmid="description" name="still-&-sanitized" content="& I will not be <sanitized>">
|
||||
```
|
||||
|
||||
:warning: **Using this option is not recommended unless you know exactly what you are doing.** By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
|
||||
|
||||
#### `changed` (Function)
|
||||
|
||||
Will be called when the client `metaInfo` updates/changes. Receives the following parameters:
|
||||
- `newInfo` (Object) - The new state of the `metaInfo` object.
|
||||
- `addedTags` ([HTMLElement]) - a list of elements that were added.
|
||||
- `removedTags` ([HTMLElement]) - a list of elements that were removed.
|
||||
|
||||
`this` context is the component instance `changed` is defined on.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
changed (newInfo, addedTags, removedTags) {
|
||||
console.log('Meta info was updated!')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How `metaInfo` is Resolved
|
||||
|
||||
You can define a `metaInfo` property on any component in the tree. Child components that have `metaInfo` will recursively merge their `metaInfo` into the parent context, overwriting any duplicate properties. To better illustrate, consider this component heirarchy:
|
||||
|
||||
```html
|
||||
<parent>
|
||||
<child></child>
|
||||
</parent>
|
||||
```
|
||||
|
||||
If both `<parent>` _and_ `<child>` define a `title` property inside `metaInfo`, then the `title` that gets rendered will resolve to the `title` defined inside `<child>`.
|
||||
|
||||
#### Lists of Tags
|
||||
|
||||
When specifying an array in `metaInfo`, like in the below examples, the default behaviour is to simply concatenate the lists.
|
||||
|
||||
**Input:**
|
||||
```js
|
||||
// parent component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'description', content: 'foo' }
|
||||
]
|
||||
}
|
||||
}
|
||||
// child component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ name: 'description', content: 'bar' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="foo">
|
||||
<meta name="description" content="bar">
|
||||
```
|
||||
|
||||
This is not what we want, since the meta `description` needs to be unique for every page. If you want to change this behaviour such that `description` is instead replaced, then give it a `vmid`:
|
||||
|
||||
**Input:**
|
||||
```js
|
||||
// parent component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ vmid: 'description', name: 'description', content: 'foo' }
|
||||
]
|
||||
}
|
||||
}
|
||||
// child component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ vmid: 'description', name: 'description', content: 'bar' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta vmid="description" name="description" content="bar">
|
||||
```
|
||||
|
||||
While solutions like `react-helmet` manage the occurrence order and merge behaviour for you automatically, it involves a lot more code and is therefore prone to failure in some edge-cases, whereas this method is _almost_ bulletproof because of its versatility; _at the expense of one tradeoff:_ these `vmid` properties will be rendered out in the final markup (`vue-meta` uses these client-side to prevent duplicating or overriding markup). If you are serving your content GZIP'ped, then the slight increase in HTTP payload size is negligible.
|
||||
|
||||
# Performance
|
||||
|
||||
On the client, `vue-meta` batches DOM updates using [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). It needs to do this because it registers a Vue mixin that subscribes to the [`beforeMount`](https://vuejs.org/api/#beforeMount) lifecycle hook on all components in order to be notified that renders have occurred and data is ready. If `vue-meta` did not batch updates, the DOM meta info would be re-calculated and re-updated for every component on the page in quick-succession.
|
||||
|
||||
Thanks to batch updating, the update will only occurr once - even if the correct meta info has already been compiled by the server. If you don't want this behaviour, see below.
|
||||
|
||||
### How to prevent the update on the initial page render
|
||||
|
||||
Add the `data-vue-meta-server-rendered` attribute to the `<html>` tag on the server-side:
|
||||
|
||||
```html
|
||||
<html data-vue-meta-server-rendered>
|
||||
...
|
||||
```
|
||||
|
||||
`vue-meta` will check for this attribute whenever it attempts to update the DOM - if it exists, `vue-meta` will just remove it and perform no updates. If it does not exist, `vue-meta` will perform updates as usual.
|
||||
|
||||
> **Note:** While this may seem verbose, it _is_ intentional. Having `vue-meta` handle this for you automatically would limit interoperability with other server-side programming languages. If you use PHP to power your server, for example, you might also have meta info handled on the server already and want to prevent this extraneous update.
|
||||
|
||||
# FAQ
|
||||
|
||||
Here are some answers to some frequently asked questions.
|
||||
|
||||
## How do I use component props and/or component data in `metaInfo`?
|
||||
|
||||
Easy. Instead of defining `metaInfo` as an object, define it as a function and access `this` as usual:
|
||||
|
||||
**Post.vue:**
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{{ title }}}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'post',
|
||||
props: ['title'],
|
||||
data () {
|
||||
return {
|
||||
description: 'A blog post about some stuff'
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.title,
|
||||
meta: [
|
||||
{ vmid: 'description', name: 'description', content: this.description }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**PostContainer.vue:**
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<post :title="title"></post>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Post from './Post.vue'
|
||||
|
||||
export default {
|
||||
name: 'post-container',
|
||||
components: { Post },
|
||||
data () {
|
||||
return {
|
||||
title: 'Example blog post'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## How do I populate `metaInfo` from the result of an asynchronous action?
|
||||
|
||||
`vue-meta` will do this for you automatically when your component state changes.
|
||||
|
||||
Just make sure that you're using the function form of `metaInfo`:
|
||||
|
||||
```js
|
||||
{
|
||||
data () {
|
||||
return {
|
||||
title: 'Foo Bar Baz'
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.title
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Check out the [vuex-async](https://github.com/nuxt/vue-meta/tree/master/examples/vuex-async) example for a far more detailed demonstration if you have doubts.
|
||||
|
||||
Credit & Thanks for this feature goes to [Sébastien Chopin](https://github.com/Atinux).
|
||||
|
||||
## Why doesn't `vue-meta` support `jsnext:main`?
|
||||
|
||||
Originally, it did - however, it caused [problems](https://github.com/nuxt/vue-meta/issues/25). Essentially, Vue [does not support](https://github.com/vuejs/vue/issues/2880) `jsnext:main`, and does not introspect for the `default` property that is transpiled from the ES2015 source, thus breaking module resolution.
|
||||
|
||||
Given that `jsnext:main` is a non-standard property that won't stick around for long, and `vue-meta` is bundled into one file with no dynamic module internals as well as the fact that if you're using `vue-meta`, you're 99.9% likely to not be using it conditionally - the decision has been made to drop support for it entirely.
|
||||
|
||||
If this were not the case, you would have to instruct Babel to convert `default` imports to the proper commonjs module syntax via a plugin, which is not ideal since many users in the Vue landscape write their code in TypeScript, not Babel.
|
||||
|
||||
# Examples
|
||||
|
||||
To run the examples; clone this repository & run `npm install` in the root directory, and then run `npm run dev`. Head to http://localhost:8080.
|
||||
[MIT](./LICENSE.md)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["dynamic-import-node"],
|
||||
"presets": [
|
||||
[ "@babel/env", {
|
||||
targets: {
|
||||
node: "current"
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
module.exports = {
|
||||
locales: {
|
||||
'/': {
|
||||
lang: 'en-US',
|
||||
title: 'Vue Meta',
|
||||
description: 'Metadata manager for Vue.js'
|
||||
},
|
||||
},
|
||||
ga: 'UA-88662854-1',
|
||||
serviceWorker: true,
|
||||
themeConfig: {
|
||||
repo: 'nuxt/vue-meta',
|
||||
docsDir: 'docs',
|
||||
locales: {
|
||||
'/': {
|
||||
label: 'English',
|
||||
selectText: 'Languages',
|
||||
editLinkText: 'Edit this page on GitHub',
|
||||
nav: [{
|
||||
text: 'Guide',
|
||||
link: '/guide/'
|
||||
}, {
|
||||
text: 'API',
|
||||
link: '/api/'
|
||||
}, {
|
||||
text: 'Release Notes',
|
||||
link: 'https://github.com/nuxt/vue-meta/releases'
|
||||
}],
|
||||
sidebar: [
|
||||
'/',
|
||||
{
|
||||
title: 'Getting started',
|
||||
collapsable: false,
|
||||
children: [
|
||||
'/guide/',
|
||||
'/guide/preparing',
|
||||
'/guide/ssr',
|
||||
'/guide/frameworks'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Usage',
|
||||
collapsable: false,
|
||||
children: [
|
||||
'/guide/metainfo',
|
||||
'/guide/special',
|
||||
'/guide/caveats',
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'FAQ',
|
||||
collapsable: false,
|
||||
children: [
|
||||
'/faq/',
|
||||
'/faq/performance.md',
|
||||
'/faq/prevent-initial.md',
|
||||
'/faq/component-props.md',
|
||||
'/faq/async-action.md',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,14 @@
|
||||
<img src="vue-meta.png" alt="vue-meta"/>
|
||||
|
||||
::: tip We need your help
|
||||
We are working on defining the RFC for Vue Meta v3.0. It will be a ground-breaking release built from the ground up.
|
||||
|
||||
We would like your help with this! Please visit the [Vue Meta v3.0 rfc](https://github.com/nuxt/rfcs/issues/19) and let us know your thoughts.
|
||||
:::
|
||||
|
||||
# Introduction
|
||||
`vue-meta` is a [Vue.js](https://vuejs.org) plugin that allows you to manage your app's metadata, much like [`react-helmet`](https://github.com/nfl/react-helmet) does for React. However, instead of setting your data as props passed to a proprietary component, you simply export it as part of your component's data using the `metaInfo` property.
|
||||
|
||||
These properties, when set on a deeply nested component, will cleverly overwrite their parent components' `metaInfo`, thereby enabling custom info for each top-level view as well as coupling metadata directly to deeply nested sub components for more maintainable code.
|
||||
|
||||
[Get started](/guide) or play with the [examples](https://github.com/nuxt/vue-meta/tree/master/examples)
|
||||
@@ -0,0 +1,470 @@
|
||||
---
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
## Plugin options
|
||||
|
||||
### keyName
|
||||
- type `string`
|
||||
- default `metaInfo`
|
||||
|
||||
The name of the component option that contains all the information that gets converted to the various meta tags & attributes for the page
|
||||
|
||||
### attribute
|
||||
- type `string`
|
||||
- default `data-vue-meta`
|
||||
|
||||
The name of the attribute vue-meta arguments on elements to know which it should manage and which it should ignore.
|
||||
|
||||
### ssrAttribute
|
||||
- type `string`
|
||||
- default `data-vue-meta-server-rendered`
|
||||
|
||||
The name of the attribute that is added to the `html` tag to inform `vue-meta` that the server has already generated the meta tags for the initial render
|
||||
|
||||
See [How to prevent update on page load](/faq/prevent-initial)
|
||||
|
||||
### tagIDKeyName
|
||||
- type `string`
|
||||
- default `vmid`
|
||||
|
||||
The property that tells `vue-meta` to overwrite (instead of append) an item in a tag list.
|
||||
For example, if you have two `meta` tag list items that both have `vmid` of 'description',
|
||||
then vue-meta will overwrite the shallowest one with the deepest one.
|
||||
|
||||
### contentKeyName
|
||||
- type `string`
|
||||
- default `content`
|
||||
|
||||
The key name for the content-holding property
|
||||
|
||||
### metaTemplateKeyName
|
||||
- type `string`
|
||||
- default `template`
|
||||
|
||||
The key name for possible meta templates
|
||||
|
||||
### refreshOnceOnNavigation
|
||||
- type `boolean`
|
||||
- default `false`
|
||||
|
||||
When `true` then `vue-meta` will pause updates once page navigation starts and resumes updates when navigation finishes (resuming also triggers an update).
|
||||
This could both be a performance improvement as a possible fix for 'flickering' when you are e.g. replacing stylesheets
|
||||
|
||||
## Plugin methods
|
||||
|
||||
The `vue-meta` plugin injects a `$meta()` function in the Vue prototype which provides the following methods
|
||||
|
||||
:::tip Note
|
||||
`$meta()` is a function so we only need to insert it once in the `Vue.prototype`, but still use `this` to reference the component it was called from
|
||||
:::
|
||||
|
||||
### $meta().getOptions
|
||||
- returns [`pluginOptions`](/api/#plugin-options)
|
||||
|
||||
Could be used by third-party libraries who wish to interact with `vue-meta`
|
||||
|
||||
### $meta().refresh
|
||||
- returns [`metaInfo`](/api/#metaInfo-properties)
|
||||
|
||||
Updates the current metadata with new metadata.
|
||||
Useful when updating metadata as the result of an asynchronous action that resolves after the initial render takes place.
|
||||
|
||||
### $meta().inject
|
||||
- returns [`metaInfo`](/api/#metaInfo-properties)
|
||||
|
||||
:::tip SSR only
|
||||
`inject` is available in the server plugin only and is not available on the client
|
||||
:::
|
||||
|
||||
It returns a special `metaInfo` object where all keys have an object as value which contains a `text()` method for returning html code
|
||||
|
||||
See [Rendering with renderToString](/guide/ssr.html#simple-rendering-with-rendertostring) for an example
|
||||
|
||||
### $meta().pause
|
||||
- arguments:
|
||||
- refresh (type `boolean`, default `false`)
|
||||
- returns `resume()`
|
||||
|
||||
Pauses global metadata updates until either the returned resume method is called or [resume](/api/#meta-resume)
|
||||
|
||||
### $meta().resume
|
||||
- arguments:
|
||||
- refresh (type `boolean`, default `false`)
|
||||
- returns [`metaInfo`](/api/#metaInfo-properties) (optional)
|
||||
|
||||
Resumes metadata updates after they have been paused. If `refresh` is `true` it immediately initiates a metadata update by calling [refresh](/api/#meta-refresh)
|
||||
|
||||
## metaInfo properties
|
||||
|
||||
::: tip Note
|
||||
The documentation below uses `metaInfo` as `keyName` in the examples, please note that this is [configurable](/api/#keyname) and could be different in your case
|
||||
:::
|
||||
|
||||
### title
|
||||
- type `string`
|
||||
|
||||
Maps to the inner-text value of the `<title>` element.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: 'Foo Bar'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title>Foo Bar</title>
|
||||
```
|
||||
|
||||
### titleTemplate
|
||||
- type `string | Function`
|
||||
|
||||
The value of `title` will be injected into the `%s` placeholder in `titleTemplate` before being rendered. The original title will be available on `metaInfo.titleChunk`.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: 'Foo Bar',
|
||||
titleTemplate: '%s - Baz'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title>Foo Bar - Baz</title>
|
||||
```
|
||||
|
||||
The property can also be a function:
|
||||
|
||||
```js
|
||||
titleTemplate: (titleChunk) => {
|
||||
// If undefined or blank then we don't need the hyphen
|
||||
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
|
||||
}
|
||||
```
|
||||
|
||||
### htmlAttrs
|
||||
### headAttrs
|
||||
### bodyAttrs
|
||||
- type `object`
|
||||
|
||||
Each **key:value** maps to the equivalent **attribute:value** of the `<body>` element.
|
||||
|
||||
Since `v2.0` value can also be an `Array<string>`
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
amp: true
|
||||
},
|
||||
bodyAttrs: {
|
||||
class: ['dark-mode', 'mobile']
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<html lang="en" amp>
|
||||
<body class="dark-mode mobile">Foo Bar</body>
|
||||
```
|
||||
|
||||
### base
|
||||
- type `object`
|
||||
|
||||
Maps to a newly-created `<base>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
base: { target: '_blank', href: '/' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<base target="_blank" href="/">
|
||||
```
|
||||
|
||||
### meta
|
||||
- type `collection`
|
||||
|
||||
Each item in the array maps to a newly-created `<meta>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
#### Content templates
|
||||
|
||||
Since `v1.5.0`, you can now set up meta templates that work similar to the titleTemplate:
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{
|
||||
'property': 'og:title',
|
||||
'content': 'Test title',
|
||||
'template': chunk => `${chunk} - My page`, //or as string template: '%s - My page',
|
||||
'vmid': 'og:title'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta name="og:title" property="og:title" content="Test title - My page">
|
||||
```
|
||||
|
||||
### link
|
||||
- type `collection`
|
||||
|
||||
|
||||
Each item in the array maps to a newly-created `<link>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: '/css/index.css' },
|
||||
{ rel: 'favicon', href: 'favicon.ico' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/css/index.css">
|
||||
<link rel="favicon" href="favicon.ico">
|
||||
```
|
||||
|
||||
### style
|
||||
- type `object`
|
||||
|
||||
Each item in the array maps to a newly-created `<style>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
style: [
|
||||
{ cssText: '.foo { color: red }', type: 'text/css' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<style type="text/css">.foo { color: red }</style>
|
||||
```
|
||||
|
||||
### script
|
||||
- type `collection`
|
||||
|
||||
Each item in the array maps to a newly-created `<script>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
script: [
|
||||
{ src: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.js', async: true, defer: true }
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" async defer></script>
|
||||
```
|
||||
|
||||
#### Add json or other raw data
|
||||
::: warning
|
||||
You have to disable sanitizers so the content of `innerHTML` won't be escaped. Please see [__dangerouslyDisableSanitizersByTagID](/api/#dangerouslydisablesanitizersbytagid) for more info on related risks
|
||||
:::
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
script: [{
|
||||
vmid: 'ldjson-schema',
|
||||
innerHTML: '{ "@context": "http://schema.org" }',
|
||||
type: 'application/ld+json'
|
||||
}],
|
||||
__dangerouslyDisableSanitizersByTagID: {
|
||||
'ldjson-schema': ['innerHTML']
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">{ "@context": "http://schema.org" }</script>
|
||||
```
|
||||
#### Special attribute: `body: true`
|
||||
|
||||
If you e.g. wish to force delayed execution of a script or just want the script to be included in the `<body>` of the page, add `body: true`.
|
||||
Script tags with `body: true` are rendered just before `</body>`
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
script: [{
|
||||
innerHTML: 'console.log("I am in body");',
|
||||
type: 'text/javascript',
|
||||
body: true
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### noscript
|
||||
- type `collection`
|
||||
|
||||
Each item in the array maps to a newly-created `<noscript>` element, where object properties map to attributes.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
noscript: [
|
||||
{ innerHTML: 'This website requires JavaScript.' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<noscript>This website requires JavaScript.</noscript>
|
||||
```
|
||||
|
||||
### __dangerouslyDisableSanitizers
|
||||
- type `Array<string>`
|
||||
|
||||
::: danger
|
||||
If you need to disable sanitation, please always use [__dangerouslyDisableSanitizersByTagID](/api/#dangerouslydisablesanitizers) when possible
|
||||
|
||||
By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
|
||||
:::
|
||||
|
||||
By default, `vue-meta` sanitizes HTML entities in _every_ property. You can disable this behaviour on a per-property basis using `__dangerouslyDisableSantizers`. Just pass it a list of properties you want sanitization to be disabled on:
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: '<I will be sanitized>',
|
||||
meta: [{
|
||||
vmid: 'description',
|
||||
name: 'description',
|
||||
content: '& I will not be <sanitized>'
|
||||
}],
|
||||
__dangerouslyDisableSanitizers: ['meta']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title><I will be sanitized></title>
|
||||
<meta vmid="description" name="description" content="& I will not be <sanitized>">
|
||||
```
|
||||
|
||||
### __dangerouslyDisableSanitizersByTagID
|
||||
- type `object`
|
||||
|
||||
::: warning
|
||||
By disabling sanitation, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
|
||||
:::
|
||||
|
||||
Provides same functionality as `__dangerouslyDisableSanitizers` but you can specify which property for which `tagIDKeyName` sanitation should be disabled. It expects an object with the vmid as key and an array with property keys as value:
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
title: '<I will be sanitized>',
|
||||
meta: [{
|
||||
vmid: 'description',
|
||||
name: 'still-&-sanitized',
|
||||
content: '& I will not be <sanitized>'
|
||||
}],
|
||||
__dangerouslyDisableSanitizersByTagID: {
|
||||
description: ['content']
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<title><I will be sanitized></title>
|
||||
<meta vmid="description" name="still-&-sanitized" content="& I will not be <sanitized>">
|
||||
```
|
||||
|
||||
### changed
|
||||
- type `Function`
|
||||
|
||||
A callback function which is called whenever the `metaInfo` updates / changes.
|
||||
|
||||
The callback receives the following arguments:
|
||||
- **newInfo**
|
||||
- type `object`<br/>
|
||||
The updated [`metaInfo`](/api/#metaInfo-properties) object
|
||||
- **addedTags**
|
||||
- type `Array<HTMLElement>`<br/>
|
||||
List of elements that were added
|
||||
- **removedTags**
|
||||
- type `Array<HTMLElement>`<br/>
|
||||
List of elements that were removed
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
changed (newInfo, addedTags, removedTags) {
|
||||
console.log('Metadata was updated!')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### afterNavigation
|
||||
- type `Function`
|
||||
|
||||
A callback function which is called when `vue-meta` has updated the metadata after navigation occurred.
|
||||
This can be used to track page views with the updated document title etc.
|
||||
|
||||
Adding a `afterNavigation` callback behaves the same as when [refreshOnceOnNavigation](/api/#refreshonceonnavigation) is `true`
|
||||
|
||||
The callback receives the following arguments:
|
||||
- **newInfo**
|
||||
- type `object`<br/>
|
||||
The updated [`metaInfo`](/api/#metaInfo-properties) object
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
afterNavigation(metaInfo) {
|
||||
trackPageView(document.title)
|
||||
// is the same as
|
||||
trackPageView(metaInfo.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# How is `metaInfo` resolved?
|
||||
|
||||
You can define a `metaInfo` property on any component in the tree. Child components that have `metaInfo` will recursively merge their `metaInfo` into the parent context, overwriting any duplicate properties. To better illustrate, consider this component hierarchy:
|
||||
|
||||
```html
|
||||
<parent>
|
||||
<child></child>
|
||||
</parent>
|
||||
```
|
||||
|
||||
If both `<parent>` _and_ `<child>` define a `title` property inside `metaInfo`, then the `title` that gets rendered will resolve to the `title` defined inside `<child>`.
|
||||
|
||||
## Concatenate metadata
|
||||
|
||||
When specifying an array in `metaInfo`, like in the below examples, the default behaviour is to simply concatenate the lists.
|
||||
|
||||
**Input:**
|
||||
```js
|
||||
// parent component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'description', content: 'foo' }
|
||||
]
|
||||
}
|
||||
}
|
||||
// child component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ name: 'description', content: 'bar' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="foo">
|
||||
<meta name="description" content="bar">
|
||||
```
|
||||
|
||||
## Unique metadata
|
||||
|
||||
This is not what we want, since the meta `description` needs to be unique for every page. If you want to change this behaviour such that `description` is instead replaced, then give it a `vmid`:
|
||||
|
||||
**Input:**
|
||||
```js
|
||||
// parent component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ vmid: 'description', name: 'description', content: 'foo' }
|
||||
]
|
||||
}
|
||||
}
|
||||
// child component
|
||||
{
|
||||
metaInfo: {
|
||||
meta: [
|
||||
{ vmid: 'description', name: 'description', content: 'bar' }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```html
|
||||
<meta charset="utf-8">
|
||||
<meta vmid="description" name="description" content="bar">
|
||||
```
|
||||
|
||||
While solutions like `react-helmet` manage the occurrence order and merge behaviour for you automatically, it involves a lot more code and is therefore prone to failure in some edge-cases, whereas this method is _almost_ bulletproof because of its versatility; _at the expense of one tradeoff:_ these `vmid` properties will be rendered out in the final markup (`vue-meta` uses these client-side to prevent duplicating or overriding markup). If you are serving your content gzipped, then the slight increase in HTTP payload size is negligible.
|
||||
@@ -0,0 +1,22 @@
|
||||
# How to use async data in metaInfo?
|
||||
|
||||
`vue-meta` will do this for you automatically when your component state changes.
|
||||
|
||||
Just make sure that you're using the function form of `metaInfo`:
|
||||
|
||||
```js
|
||||
{
|
||||
data () {
|
||||
return {
|
||||
title: 'Foo Bar Baz'
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.title
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Check out the [vuex-async](https://github.com/nuxt/vue-meta/tree/master/examples/vuex-async) example for a more detailed demonstration
|
||||
@@ -0,0 +1,55 @@
|
||||
# How to use component props or data
|
||||
|
||||
Easy. Instead of defining `metaInfo` as an object, define it as a function and access `this` as usual:
|
||||
|
||||
**Post.vue:**
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{{ title }}}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'post',
|
||||
props: ['title'],
|
||||
data () {
|
||||
return {
|
||||
description: 'A blog post about some stuff'
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.title,
|
||||
meta: [
|
||||
{ vmid: 'description', name: 'description', content: this.description }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**PostContainer.vue:**
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<post :title="title"></post>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Post from './Post.vue'
|
||||
|
||||
export default {
|
||||
name: 'post-container',
|
||||
components: { Post },
|
||||
data () {
|
||||
return {
|
||||
title: 'Example blog post'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
# Any performance considerations?
|
||||
|
||||
Short answer, no
|
||||
|
||||
On the client, `vue-meta` batches DOM updates using [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). It needs to do this because it registers a Vue mixin that subscribes to the [`beforeMount`](https://vuejs.org/api/#beforeMount) lifecycle hook on all components in order to be notified that renders have occurred and data is ready. If `vue-meta` did not batch updates, the DOM metadata would be re-calculated and re-updated for every component on the page in quick-succession.
|
||||
|
||||
Thanks to batch updating, the update will only occurr once - even if the correct metadata has already been compiled by the server. If you don't want this behaviour, see below.
|
||||
|
||||
:::tip Improvements since v2.0
|
||||
Previous versions of vue-meta injected lifecycle hooks from the global mixin on all components on the page. Also when refreshing metadata it checked all components on the page
|
||||
|
||||
Since v2.0 runtime performance should be improved due to:
|
||||
- the global mixin injects just a `beforeCreate` lifecycle hook, other hooks are only added for components which define `metaInfo`
|
||||
- we track component branches with `vue-meta` components which means that when refreshing metadata we can skip branches without `metaInfo`
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,12 @@
|
||||
# How to prevent update on page load
|
||||
|
||||
Add the `data-vue-meta-server-rendered` attribute to the `<html>` tag on the server-side:
|
||||
|
||||
```html
|
||||
<html data-vue-meta-server-rendered>
|
||||
...
|
||||
```
|
||||
|
||||
`vue-meta` will check for this attribute whenever it attempts to update the DOM - if it exists, `vue-meta` will just remove it and perform no updates. If it does not exist, `vue-meta` will perform updates as usual.
|
||||
|
||||
While this may seem verbose, it _is_ intentional. Having `vue-meta` handle this for you automatically would limit interoperability with other server-side programming languages. If you use PHP to power your server, for example, you might also have metadata handled on the server already and want to prevent this extraneous update.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Installation
|
||||
|
||||
:::tip Using a framework?
|
||||
|
||||
Are you using a framework like Nuxt.js, Gridsome or another one which uses vue-meta? Then `vue-meta` should already be installed and you can skip to [Usage](/guide/metainfo.html) or consult the [documentation](/guide/frameworks.html) of your framework for more information.
|
||||
:::
|
||||
|
||||
## Download / CDN
|
||||
|
||||
[https://unpkg.com/vue-meta/lib/vue-meta.js](https://unpkg.com/vue-meta/lib/vue-meta.js)
|
||||
|
||||
For the latest version in the v1.x branch you can use:<br/>
|
||||
[https://unpkg.com/vue-meta@1/lib/vue-meta.js](https://unpkg.com/vue-meta@1/lib/vue-meta.js)
|
||||
|
||||
Or you can replace `1` with the full version number you wish to use.
|
||||
|
||||
If you include vue-meta after Vue it will install automatically
|
||||
|
||||
**Unminified (suggested only for dev):**
|
||||
```html
|
||||
<script src="https://unpkg.com/vue-meta/lib/vue-meta.js"></script>
|
||||
```
|
||||
|
||||
**Minified:**
|
||||
```html
|
||||
<script src="https://unpkg.com/vue-meta/lib/vue-meta.min.js"></script>
|
||||
```
|
||||
|
||||
## Package manager
|
||||
**Yarn**
|
||||
```sh
|
||||
$ yarn add vue-meta
|
||||
```
|
||||
|
||||
**npm**
|
||||
```sh
|
||||
$ npm i vue-meta
|
||||
```
|
||||
|
||||
### Install
|
||||
|
||||
:::warning Using a framework?
|
||||
|
||||
If you use a framework like Nuxt.js or Gridsome, vue-meta comes pre-installed and this step is most likely **not** required. Consult the [documentation](/guide/frameworks.html) of your framework for more information
|
||||
:::
|
||||
|
||||
If you add `vue-meta` with a package manager, you will need to install the `vue-meta` plugin manually:
|
||||
|
||||
```js
|
||||
import Vue from 'vue'
|
||||
import VueMeta from 'vue-meta'
|
||||
|
||||
Vue.use(VueMeta)
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
# Caveats
|
||||
|
||||
## Reactive variables in template functions
|
||||
|
||||
Both [title](/api/#titletemplate) as [meta](/api/#content-templates) support using template function.
|
||||
Due to how Vue determines reactivity it is not possible to use reactive variables directly in template functions
|
||||
|
||||
```js
|
||||
{
|
||||
// this wont work
|
||||
metaInfo() {
|
||||
return {
|
||||
titleTemplate: chunk => (
|
||||
this.locale === 'nl-NL'
|
||||
? `${chunk} - Welkom`
|
||||
: `${chunk} - Welcome`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You need to assign the reactive variable to a local variable first for this to work:
|
||||
|
||||
```js
|
||||
{
|
||||
// this will work
|
||||
metaInfo() {
|
||||
const locale = this.locale
|
||||
return {
|
||||
titleTemplate: chunk => (
|
||||
locale === 'nl-NL'
|
||||
? `${chunk} - Welkom`
|
||||
: `${chunk} - Welcome`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# Frameworks
|
||||
|
||||
If you are using a framework which uses `vue-meta`, please make sure to consult their documentation first
|
||||
|
||||
## Gridsome
|
||||
|
||||
Proceed to the [Gridsome documentation](https://gridsome.org/docs)
|
||||
|
||||
## Nuxt.js
|
||||
|
||||
Proceed to the [Nuxt.js documentation](https://nuxtjs.org/api)
|
||||
@@ -0,0 +1,69 @@
|
||||
# Defining `metaInfo`
|
||||
|
||||
You can define a `[keyName]` property in any of your components, by default this is `metaInfo`.
|
||||
|
||||
See the [API](/api) for a list of recognised `metaInfo` properties
|
||||
|
||||
::: tip Note
|
||||
Altough we talk about the `metaInfo` variable on this page, please note that the keyName is [configurable](/api/#keyname) and could be different in your case. E.g. [Nuxt.js](https://nuxtjs.org/api/pages-head#the-head-method) uses `head` as keyName
|
||||
:::
|
||||
|
||||
**App.vue:**
|
||||
```html
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
metaInfo: {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: 'Default Title',
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | My Awesome Webapp'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Home.vue**
|
||||
```html
|
||||
<template>
|
||||
<div id="page">
|
||||
<h1>Home Page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home',
|
||||
metaInfo: {
|
||||
title: 'My Awesome Webapp',
|
||||
// override the parent template and just use the above title only
|
||||
titleTemplate: null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**About.vue**
|
||||
```html
|
||||
<template>
|
||||
<div id="page">
|
||||
<h1>About Page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'About',
|
||||
metaInfo: {
|
||||
// title will be injected into parent titleTemplate
|
||||
title: 'About Us'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# Preparing the plugin
|
||||
:::tip Note
|
||||
This step is optional if you don't need SSR and `Vue` is available as a global variable. `vue-meta` will install itself in this case.
|
||||
:::
|
||||
|
||||
In order to use this plugin, you first need to pass it to `Vue.use` - if you're not rendering on the server-side, your entry file will suffice. If you are rendering on the server, then place it in a file that runs both on the server and on the client before your root instance is mounted. If you're using [`vue-router`](https://github.com/vuejs/vue-router), then your main `router.js` file is a good place:
|
||||
|
||||
**router.js:**
|
||||
```js
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Meta from 'vue-meta'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(Meta)
|
||||
|
||||
export default new Router({
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
`vue-meta` allows a few custom options:
|
||||
|
||||
```js
|
||||
Vue.use(Meta, {
|
||||
keyName: 'metaInfo',
|
||||
attribute: 'data-vue-meta',
|
||||
ssrAttribute: 'data-vue-meta-server-rendered',
|
||||
tagIDKeyName: 'vmid',
|
||||
refreshOnceOnNavigation: true
|
||||
})
|
||||
```
|
||||
|
||||
See the [API](/api/#plugin-options) for a description of the available plugin options
|
||||
@@ -0,0 +1,108 @@
|
||||
# Special cases
|
||||
|
||||
::: tip Read first
|
||||
Understanding [How is metaInfo resolved?](/faq/#concatenate-metadata) is probably a prerequisite for these cases
|
||||
:::
|
||||
|
||||
## Remove parent property by child
|
||||
|
||||
If a child returns `null` as content value then the parent metaInfo property with the same `vmid` will be ignored
|
||||
|
||||
:::tip Content value
|
||||
With content value we mean the following value of a `metaInfo` property:
|
||||
- the value of a key for `object` types as [`htmlAttrs`](/api/#htmlattrs)
|
||||
- the value of `[contentKeyName]` or `innerHTML` keys for `collection` types as [`meta`](/api/#meta)
|
||||
:::
|
||||
|
||||
The following might be a bit far-fetched, but its just an example
|
||||
```js
|
||||
// parent
|
||||
metaInfo: {
|
||||
style: [{
|
||||
vmid: 'page-load-overlay',
|
||||
innerHTML: `
|
||||
body div.loading {
|
||||
z-index: 999;
|
||||
background-color: #0f0f0f;
|
||||
opacity: 0.9;
|
||||
}
|
||||
`,
|
||||
}]
|
||||
}
|
||||
|
||||
// dynamically loaded child
|
||||
metaInfo() {
|
||||
const style = this.cssTexts
|
||||
return { style }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
this.cssTexts: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.cssTexts.push({
|
||||
vmid: 'page-load-overlay',
|
||||
innerHTML: null
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Use child property conditionally
|
||||
|
||||
If you wish to use a child property conditionally and use the parents' property as default value, make sure the child returns `undefined` as content value
|
||||
|
||||
:::tip Content value
|
||||
With content value we mean the following value of a `metaInfo` property:
|
||||
- the value of a key for `object` types as [`htmlAttrs`](/api/#htmlattrs)
|
||||
- the value of `[contentKeyName]` or `innerHTML` keys for `collection` types as [`meta`](/api/#meta)
|
||||
:::
|
||||
|
||||
The below example will still show a description when the GET in the child fails
|
||||
```js
|
||||
// parent
|
||||
metaInfo: {
|
||||
meta: [{
|
||||
vmid: 'description',
|
||||
name: 'description',
|
||||
content: 'my standard description',
|
||||
}]
|
||||
}
|
||||
|
||||
// child
|
||||
metaInfo() {
|
||||
return {
|
||||
meta: [{
|
||||
vmid: 'description',
|
||||
name: 'description',
|
||||
content: this.description,
|
||||
}]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
description: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDescription() {
|
||||
this.description = this.$axios.get()
|
||||
if (!this.description) {
|
||||
// if GET request failed or returned empty,
|
||||
// explicitly set to undefined so the parents'
|
||||
// default description is used
|
||||
this.description = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Boolean attributes
|
||||
|
||||
`vue-meta` maintains a [list](https://github.com/nuxt/vue-meta/blob/master/src/shared/constants.js) of attributes which are Boolean attributes according to the HTML specs (and some extra). Whatever value you will pass to these attributes, they will be rendered as a Boolean attribute.<sup>*</sup>
|
||||
|
||||
<sup>*</sup><small>Except for the special values `undefined` and `null`, see above</small>
|
||||
|
||||
:::tip Note
|
||||
Prior to `v2.0` any attribute key with `undefined` as value was rendered as Boolean attribute. This has been removed as bundlers often remove object properties with an `undefined` value as given `a = {}` then `a.a === undefined`
|
||||
:::
|
||||
@@ -0,0 +1,117 @@
|
||||
# Server Side Rendering
|
||||
|
||||
If you have an isomorphic/universal web application, you'll likely want to render your metadata on the server side as well. Here's how.
|
||||
|
||||
## Add `vue-meta` to the context
|
||||
|
||||
You'll need to expose the results of the `$meta` method that `vue-meta` adds to the Vue instance to the bundle render context before you can begin injecting your metadata. You'll need to do this in your server entry file:
|
||||
|
||||
**server-entry.js:**
|
||||
```js
|
||||
import app from './app'
|
||||
|
||||
const router = app.$router
|
||||
const meta = app.$meta() // here
|
||||
|
||||
export default (context) => {
|
||||
router.push(context.url)
|
||||
context.meta = meta // and here
|
||||
return app
|
||||
}
|
||||
```
|
||||
|
||||
## Inject metadata into page string
|
||||
|
||||
Probably the easiest method to wrap your head around is if your Vue server markup is rendered out as a string using `renderToString`:
|
||||
|
||||
**server.js:**
|
||||
|
||||
```js
|
||||
app.get('*', (req, res) => {
|
||||
const context = { url: req.url }
|
||||
renderer.renderToString(context, (error, html) => {
|
||||
if (error) return res.send(error.stack)
|
||||
const {
|
||||
title, htmlAttrs, headAttrs, bodyAttrs, link,
|
||||
style, script, noscript, meta
|
||||
} = context.meta.inject()
|
||||
return res.send(`
|
||||
<!doctype html>
|
||||
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
|
||||
<head ${headAttrs.text()}>
|
||||
${meta.text()}
|
||||
${title.text()}
|
||||
${link.text()}
|
||||
${style.text()}
|
||||
${script.text()}
|
||||
${noscript.text()}
|
||||
</head>
|
||||
<body ${bodyAttrs.text()}>
|
||||
${html}
|
||||
<script src="/assets/vendor.bundle.js"></script>
|
||||
<script src="/assets/client.bundle.js"></script>
|
||||
${script.text({ body: true })}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
If you are using a separate template file, edit your head tag with
|
||||
|
||||
```html
|
||||
<head>
|
||||
{{{ meta.inject().title.text() }}}
|
||||
{{{ meta.inject().meta.text() }}}
|
||||
</head>
|
||||
```
|
||||
|
||||
Notice the use of `{{{` to avoid double escaping. Be extremely cautious when you use `{{{` with [`__dangerouslyDisableSanitizersByTagID`](/api/#dangerouslydisablesanitizersbytagid).
|
||||
|
||||
## Inject metadata into page stream
|
||||
|
||||
|
||||
A little more complex, but well worth it, is to instead stream your response. `vue-meta` supports streaming with no effort (on it's part :stuck_out_tongue_winking_eye:) thanks to Vue's clever `bundleRenderer` context injection:
|
||||
|
||||
**server.js**
|
||||
```js
|
||||
app.get('*', (req, res) => {
|
||||
const context = { url: req.url }
|
||||
const renderStream = renderer.renderToStream(context)
|
||||
renderStream.once('data', () => {
|
||||
const {
|
||||
title, htmlAttrs, headAttrs, bodyAttrs, link,
|
||||
style, script, noscript, meta
|
||||
} = context.meta.inject()
|
||||
res.write(`
|
||||
<!doctype html>
|
||||
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
|
||||
<head ${headAttrs.text()}>
|
||||
${meta.text()}
|
||||
${title.text()}
|
||||
${link.text()}
|
||||
${style.text()}
|
||||
${script.text()}
|
||||
${noscript.text()}
|
||||
</head>
|
||||
<body ${bodyAttrs.text()}>
|
||||
`)
|
||||
})
|
||||
renderStream.on('data', (chunk) => {
|
||||
res.write(chunk)
|
||||
})
|
||||
renderStream.on('end', () => {
|
||||
res.end(`
|
||||
<script src="/assets/vendor.bundle.js"></script>
|
||||
<script src="/assets/client.bundle.js"></script>
|
||||
${script.text({ body: true })}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
renderStream.on('error', (error) => {
|
||||
res.status(500).end(`<pre>${error.stack}</pre>`)
|
||||
})
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": [["@babel/preset-env", { targets: { node: "current" } }]],
|
||||
"plugins": ["dynamic-import-node"]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Vue Meta Examples
|
||||
|
||||
## Prepare examples
|
||||
|
||||
To prepare the examples to run locally, please follow these steps:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/nuxt/vue-meta
|
||||
cd examples
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Run the examples
|
||||
|
||||
When the examples are installed locally, start the example server as follows
|
||||
|
||||
```js
|
||||
yarn start
|
||||
// or
|
||||
HOST=0.0.0.0 PORT=8080 yarn start
|
||||
```
|
||||
and browse to `http://localhost:3000` or whatever you changed the host and port to
|
||||
|
||||
### SSR Example
|
||||
|
||||
The server side rendering example is available on the cli only, to run the SSR example just run
|
||||
|
||||
```bash
|
||||
yarn ssr
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
If you would like to use the examples while developing or debugging `vue-meta` features or issues, please do as follows
|
||||
|
||||
```js
|
||||
git clone https://github.com/nuxt/vue-meta
|
||||
yarn install
|
||||
cd examples
|
||||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
@@ -5,11 +5,16 @@ Vue.use(VueMeta)
|
||||
|
||||
Vue.component('child', {
|
||||
name: 'Child',
|
||||
props: ['page'],
|
||||
render (h) {
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
return h('h3', null, this.page)
|
||||
},
|
||||
metaInfo () {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.page
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@ Vue.component('foo', {
|
||||
})
|
||||
|
||||
new Vue({
|
||||
data() {
|
||||
return { showFoo: false }
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.showFoo = !this.showFoo
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div id="app">
|
||||
<h1>Kept alive foo</h1>
|
||||
@@ -20,14 +28,6 @@ new Vue({
|
||||
</keep-alive>
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return { showFoo: false }
|
||||
},
|
||||
methods: {
|
||||
show () {
|
||||
this.showFoo = !this.showFoo
|
||||
}
|
||||
},
|
||||
metaInfo: () => ({
|
||||
title: 'Keep-alive'
|
||||
})
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "vue-meta-examples",
|
||||
"version": "1.0.0",
|
||||
"description": "Examples for vue-meta",
|
||||
"main": "server.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development babel-node server.js",
|
||||
"start": "babel-node server.js",
|
||||
"ssr": "babel-node ssr"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/vue-meta.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nuxt/vue-meta/issues"
|
||||
},
|
||||
"homepage": "https://github.com/nuxt/vue-meta#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||
"consola": "^2.5.6",
|
||||
"cross-env": "^5.2.0",
|
||||
"express": "^4.16.4",
|
||||
"express-urlrewrite": "^1.2.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"lodash": "^4.17.11",
|
||||
"vue": "^2.6.6",
|
||||
"vue-loader": "^15.6.4",
|
||||
"vue-meta": "^1.5.8",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-server-renderer": "^2.6.8",
|
||||
"vue-template-compiler": "^2.6.6",
|
||||
"vuex": "^3.1.0",
|
||||
"webpack": "^4.29.5",
|
||||
"webpack-dev-server": "^3.2.0",
|
||||
"webpackbar": "^3.1.5"
|
||||
}
|
||||
}
|
||||
+13
-8
@@ -1,5 +1,6 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import consola from 'consola'
|
||||
import express from 'express'
|
||||
import rewrite from 'express-urlrewrite'
|
||||
import webpack from 'webpack'
|
||||
@@ -16,15 +17,19 @@ app.use(webpackDevMiddleware(webpack(WebpackConfig), {
|
||||
}
|
||||
}))
|
||||
|
||||
fs.readdirSync(__dirname).forEach(file => {
|
||||
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
|
||||
app.use(rewrite('/' + file + '/*', '/' + file + '/index.html'))
|
||||
}
|
||||
})
|
||||
fs.readdirSync(__dirname)
|
||||
.filter(file => file !== 'ssr')
|
||||
.forEach((file) => {
|
||||
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
|
||||
app.use(rewrite('/' + file + '/*', '/' + file + '/index.html'))
|
||||
}
|
||||
})
|
||||
|
||||
app.use(express.static(__dirname))
|
||||
|
||||
const port = process.env.PORT || 8080
|
||||
module.exports = app.listen(port, () => {
|
||||
console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`)
|
||||
const host = process.env.HOST || 'localhost'
|
||||
const port = process.env.PORT || 3000
|
||||
|
||||
module.exports = app.listen(port, host, () => {
|
||||
consola.info(`Server listening on http://${host}:${port}, Ctrl+C to stop`)
|
||||
})
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
const Vue = require('vue')
|
||||
const renderer = require('vue-server-renderer').createRenderer()
|
||||
const VueMeta = require('../')
|
||||
|
||||
Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
})
|
||||
|
||||
const vm = new Vue({
|
||||
template: '<hello/>',
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
htmlAttrs: { amp: undefined },
|
||||
meta: [
|
||||
{ hid: 'description', name: 'description', content: 'Hello World' }
|
||||
],
|
||||
script: [
|
||||
{ innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }', type: 'application/ld+json' },
|
||||
{ innerHTML: '{ "body": "yes" }', body: true, type: 'application/ld+json' }
|
||||
],
|
||||
__dangerouslyDisableSanitizers: ['script']
|
||||
},
|
||||
components: {
|
||||
Hello: {
|
||||
template: '<p>Hello</p>',
|
||||
data () {
|
||||
return { msg: 'Hello' }
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: `<b>${this.msg}</b>`,
|
||||
meta: [
|
||||
{ hid: 'description', name: 'description', content: this.msg }
|
||||
]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.msg = 'Hi!'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
renderer.renderToString(vm, function (err, html) {
|
||||
if (err) throw err
|
||||
const $meta = vm.$meta().inject()
|
||||
console.log('Title:\n' + $meta.title.text())
|
||||
console.log('\nHTML attrs:\n' + $meta.htmlAttrs.text())
|
||||
console.log('\nMeta:\n' + $meta.meta.text())
|
||||
console.log('\nHead Script:\n' + $meta.script.text())
|
||||
console.log('\nBody Script:\n' + $meta.script.text({ body: true }))
|
||||
})
|
||||
@@ -1,44 +0,0 @@
|
||||
const Vue = require('vue')
|
||||
const renderer = require('vue-server-renderer').createRenderer()
|
||||
const VueMeta = require('../')
|
||||
|
||||
Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
})
|
||||
|
||||
const vm = new Vue({
|
||||
template: '<hello/>',
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
htmlAttrs: { amp: undefined },
|
||||
meta: [
|
||||
{ hid: 'description', name: 'description', content: 'Hello World' }
|
||||
],
|
||||
script: [
|
||||
{ hid: 'schema', innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }', type: 'application/ld+json' },
|
||||
{ innerHTML: '{ "body": "yes" }', body: true, type: 'application/ld+json' }
|
||||
],
|
||||
__dangerouslyDisableSanitizersByTagID: { schema: ['innerHTML'] }
|
||||
},
|
||||
components: {
|
||||
Hello: {
|
||||
template: '<p>Hello</p>',
|
||||
metaInfo: {
|
||||
title: 'Coucou',
|
||||
meta: [
|
||||
{ hid: 'description', name: 'description', content: 'Coucou' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
renderer.renderToString(vm, function (err, html) {
|
||||
if (err) throw err
|
||||
const $meta = vm.$meta().inject()
|
||||
console.log('Title:\n' + $meta.title.text())
|
||||
console.log('\nHTML attrs:\n' + $meta.htmlAttrs.text())
|
||||
console.log('\nMeta:\n' + $meta.meta.text())
|
||||
console.log('\nHead Script:\n' + $meta.script.text())
|
||||
console.log('\nBody Script:\n' + $meta.script.text({ body: true }))
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import Vue from 'vue'
|
||||
// import VueMeta from 'vue-meta'
|
||||
|
||||
export default async function createApp() {
|
||||
// the dynamic import is for this example only
|
||||
const vueMetaModule = process.env.NODE_ENV === 'development' ? '../../' : 'vue-meta'
|
||||
const VueMeta = await import(vueMetaModule).then(m => m.default || m)
|
||||
|
||||
Vue.use(VueMeta, {
|
||||
tagIDKeyName: 'hid'
|
||||
})
|
||||
|
||||
return new Vue({
|
||||
components: {
|
||||
Hello: {
|
||||
template: '<p>Hello</p>',
|
||||
metaInfo: {
|
||||
title: 'Coucou',
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'Coucou'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<hello/>',
|
||||
metaInfo: {
|
||||
title: 'Hello',
|
||||
htmlAttrs: { amp: true },
|
||||
meta: [
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: 'Hello World'
|
||||
}
|
||||
],
|
||||
script: [
|
||||
{
|
||||
hid: 'ldjson-schema',
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
|
||||
}, {
|
||||
type: 'application/ld+json',
|
||||
innerHTML: '{ "body": "yes" }',
|
||||
body: true
|
||||
}
|
||||
],
|
||||
__dangerouslyDisableSanitizersByTagID: {
|
||||
'ldjson-schema': ['innerHTML']
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html data-vue-meta-server-rendered {{ htmlAttrs.text() }}>
|
||||
<head {{ headAttrs.text() }}>
|
||||
{{ meta.text() }}
|
||||
{{ title.text() }}
|
||||
{{ link.text() }}
|
||||
{{ style.text() }}
|
||||
{{ webpackAssets }}
|
||||
{{ script.text() }}
|
||||
{{ noscript.text() }}
|
||||
</head>
|
||||
<body {{ bodyAttrs.text() }}>
|
||||
{{ app }}
|
||||
{{ script.text({ body: true }) }}
|
||||
{{ noscript.text({ body: true }) }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
import createApp from './app'
|
||||
|
||||
createApp().$mount('#app')
|
||||
@@ -0,0 +1,36 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import template from 'lodash/template'
|
||||
import { createRenderer } from 'vue-server-renderer'
|
||||
import consola from 'consola'
|
||||
import createApp from './server-entry'
|
||||
|
||||
const renderer = createRenderer()
|
||||
|
||||
async function createPage() {
|
||||
const templateFile = path.resolve(__dirname, 'app.template.html')
|
||||
const templateContent = await fs.readFile(templateFile, { encoding: 'utf8' })
|
||||
|
||||
// see: https://lodash.com/docs#template
|
||||
const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
|
||||
|
||||
const webpackAssets = '<link rel="stylesheet" href="../global.css">'
|
||||
const serverApp = await createApp()
|
||||
const appHtml = await renderer.renderToString(serverApp)
|
||||
|
||||
const pageHtml = compiled({
|
||||
app: appHtml,
|
||||
webpackAssets,
|
||||
...serverApp.$meta().inject()
|
||||
})
|
||||
|
||||
return pageHtml
|
||||
}
|
||||
|
||||
consola.info(`Creating ssr page`)
|
||||
createPage()
|
||||
.then((pageHtml) => {
|
||||
consola.info(`Done, page:`)
|
||||
consola.log(pageHtml)
|
||||
})
|
||||
.catch(e => consola.error(e))
|
||||
@@ -0,0 +1,3 @@
|
||||
import createApp from './app'
|
||||
|
||||
export default createApp
|
||||
@@ -1,29 +1,48 @@
|
||||
import assign from 'object-assign'
|
||||
import Vue from 'vue'
|
||||
import VueMeta from 'vue-meta'
|
||||
import Router from 'vue-router'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(VueMeta)
|
||||
Vue.use(VueMeta, {
|
||||
refreshOnceOnNavigation: true
|
||||
})
|
||||
|
||||
let metaUpdated = 'no'
|
||||
const ChildComponent = {
|
||||
name: `child-component`,
|
||||
props: ['page'],
|
||||
template: `<h3>You're looking at the <strong>{{ page }}</strong> page</h3>`,
|
||||
metaInfo () {
|
||||
template: `<div>
|
||||
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
||||
<p>Has metaInfo been updated? {{ metaUpdated }}</p>
|
||||
</div>`,
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.page
|
||||
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
|
||||
afterNavigation() {
|
||||
metaUpdated = 'yes'
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
date: null,
|
||||
metaUpdated
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setInterval(() => {
|
||||
this.date = new Date()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// this wrapper function is not a requirement for vue-router,
|
||||
// just a demonstration that render-function style components also work.
|
||||
// See https://github.com/declandewet/vue-meta/issues/9 for more info.
|
||||
function view (page) {
|
||||
// See https://github.com/nuxt/vue-meta/issues/9 for more info.
|
||||
function view(page) {
|
||||
return {
|
||||
name: `section-${page}`,
|
||||
render (h) {
|
||||
render(h) {
|
||||
return h(ChildComponent, {
|
||||
props: { page }
|
||||
})
|
||||
@@ -41,6 +60,7 @@ const router = new Router({
|
||||
})
|
||||
|
||||
const App = {
|
||||
router,
|
||||
template: `
|
||||
<div id="app">
|
||||
<h1>vue-router</h1>
|
||||
@@ -54,6 +74,6 @@ const App = {
|
||||
`
|
||||
}
|
||||
|
||||
const app = new Vue(assign(App, { router }))
|
||||
const app = new Vue(App)
|
||||
|
||||
app.$mount('#app')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assign from 'object-assign'
|
||||
import Vue from 'vue'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = new Vue(assign(App, { router, store }))
|
||||
App.router = router
|
||||
App.store = store
|
||||
|
||||
app.$mount('#app')
|
||||
new Vue(App).$mount('#app')
|
||||
|
||||
@@ -35,33 +35,33 @@ export default new Vuex.Store({
|
||||
|
||||
// GETTERS
|
||||
getters: {
|
||||
isLoading (state) {
|
||||
isLoading(state) {
|
||||
return state.isLoading
|
||||
},
|
||||
post (state) {
|
||||
post(state) {
|
||||
return state.post
|
||||
},
|
||||
publishedPosts (state) {
|
||||
return state.posts.filter((post) => post.published)
|
||||
publishedPosts(state) {
|
||||
return state.posts.filter(post => post.published)
|
||||
},
|
||||
publishedPostsCount (state, getters) {
|
||||
publishedPostsCount(state, getters) {
|
||||
return getters.publishedPosts.length
|
||||
}
|
||||
},
|
||||
|
||||
// MUTATIONS
|
||||
mutations: {
|
||||
loadingState (state, { isLoading }) {
|
||||
loadingState(state, { isLoading }) {
|
||||
state.isLoading = isLoading
|
||||
},
|
||||
getPost (state, { slug }) {
|
||||
state.post = state.posts.find((post) => post.slug === slug)
|
||||
getPost(state, { slug }) {
|
||||
state.post = state.posts.find(post => post.slug === slug)
|
||||
}
|
||||
},
|
||||
|
||||
// ACTIONS
|
||||
actions: {
|
||||
getPost ({ commit }, payload) {
|
||||
getPost({ commit }, payload) {
|
||||
commit('loadingState', { isLoading: true })
|
||||
setTimeout(() => {
|
||||
commit('getPost', payload)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assign from 'object-assign'
|
||||
import Vue from 'vue'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = new Vue(assign(App, { router, store }))
|
||||
App.router = router
|
||||
App.store = store
|
||||
|
||||
app.$mount('#app')
|
||||
new Vue(App).$mount('#app')
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Meta from 'vue-meta'
|
||||
import Home from './views/Home.vue'
|
||||
import Post from './views/Post.vue'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(Meta)
|
||||
|
||||
const Home = () => import('./views/Home.vue')
|
||||
const Post = () => import('./views/Post.vue')
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
base: '/vuex',
|
||||
|
||||
@@ -35,27 +35,27 @@ export default new Vuex.Store({
|
||||
|
||||
// GETTERS
|
||||
getters: {
|
||||
post (state) {
|
||||
post(state) {
|
||||
return state.post
|
||||
},
|
||||
publishedPosts (state) {
|
||||
return state.posts.filter((post) => post.published)
|
||||
publishedPosts(state) {
|
||||
return state.posts.filter(post => post.published)
|
||||
},
|
||||
publishedPostsCount (state, getters) {
|
||||
publishedPostsCount(state, getters) {
|
||||
return getters.publishedPosts.length
|
||||
}
|
||||
},
|
||||
|
||||
// MUTATIONS
|
||||
mutations: {
|
||||
getPost (state, { slug }) {
|
||||
state.post = state.posts.find((post) => post.slug === slug)
|
||||
getPost(state, { slug }) {
|
||||
state.post = state.posts.find(post => post.slug === slug)
|
||||
}
|
||||
},
|
||||
|
||||
// ACTIONS
|
||||
actions: {
|
||||
getPost ({ commit }, payload) {
|
||||
getPost({ commit }, payload) {
|
||||
commit('getPost', payload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import webpack from 'webpack'
|
||||
import WebpackBar from 'webpackbar'
|
||||
import VueLoaderPlugin from 'vue-loader/lib/plugin'
|
||||
|
||||
export default {
|
||||
devtool: 'inline-source-map',
|
||||
mode: 'development',
|
||||
entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
|
||||
const fullDir = path.join(__dirname, dir)
|
||||
const entry = path.join(fullDir, 'app.js')
|
||||
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
|
||||
entries[dir] = entry
|
||||
}
|
||||
return entries
|
||||
}, {}),
|
||||
entry: fs.readdirSync(__dirname)
|
||||
.filter(entry => entry !== 'ssr')
|
||||
.reduce((entries, dir) => {
|
||||
const fullDir = path.join(__dirname, dir)
|
||||
const entry = path.join(fullDir, 'app.js')
|
||||
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
|
||||
entries[dir] = entry
|
||||
}
|
||||
return entries
|
||||
}, {}),
|
||||
output: {
|
||||
path: path.join(__dirname, '__build__'),
|
||||
filename: '[name].js',
|
||||
@@ -28,7 +32,7 @@ export default {
|
||||
resolve: {
|
||||
alias: {
|
||||
'vue': 'vue/dist/vue.js',
|
||||
'vue-meta': path.join(__dirname, '..', 'src')
|
||||
'vue-meta': process.env.NODE_ENV === 'development' ? path.join(__dirname, '..', 'src') : 'vue-meta'
|
||||
}
|
||||
},
|
||||
// Expose __dirname to allow automatically setting basename.
|
||||
@@ -37,7 +41,8 @@ export default {
|
||||
__dirname: true
|
||||
},
|
||||
plugins: [
|
||||
// new webpack.optimize.CommonsChunkPlugin('shared.js'),
|
||||
new WebpackBar(),
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'jest-environment-jsdom-global',
|
||||
|
||||
expand: true,
|
||||
|
||||
forceExit: false,
|
||||
|
||||
// https://github.com/facebook/jest/pull/6747 fix warning here
|
||||
// But its performance overhead is pretty bad (30+%).
|
||||
// detectOpenHandles: true
|
||||
|
||||
setupFilesAfterEnv: ['./test/utils/setup'],
|
||||
|
||||
coverageDirectory: './coverage',
|
||||
|
||||
collectCoverageFrom: [
|
||||
'**/src/**/*.js'
|
||||
],
|
||||
|
||||
coveragePathIgnorePatterns: [
|
||||
'node_modules'
|
||||
],
|
||||
|
||||
testPathIgnorePatterns: [
|
||||
'node_modules'
|
||||
],
|
||||
|
||||
transformIgnorePatterns: [
|
||||
'node_modules'
|
||||
],
|
||||
|
||||
transform: {
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
'.*\\.(vue)$': 'vue-jest'
|
||||
},
|
||||
|
||||
moduleFileExtensions: [
|
||||
'ts',
|
||||
'js',
|
||||
'json'
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import webpackConfig from './examples/webpack.config.babel'
|
||||
|
||||
delete webpackConfig.entry
|
||||
|
||||
export default (config) => {
|
||||
config.set({
|
||||
browsers: ['PhantomJS'],
|
||||
frameworks: ['mocha', 'chai'],
|
||||
reporters: ['mocha', 'coverage'],
|
||||
files: ['test/index.js'],
|
||||
preprocessors: {
|
||||
'test/index.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
coverageReporter: {
|
||||
reporters: [
|
||||
{ type: 'lcov' },
|
||||
{ type: 'text' }
|
||||
],
|
||||
includeAllSources: true,
|
||||
dir: 'coverage',
|
||||
subdir: '.'
|
||||
},
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
noInfo: true
|
||||
},
|
||||
mochaReporter: {
|
||||
showDiff: true,
|
||||
output: 'full'
|
||||
},
|
||||
singleRun: true
|
||||
})
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
require('@babel/register')
|
||||
module.exports = require('./karma.conf.babel').default
|
||||
+106
-103
@@ -1,93 +1,14 @@
|
||||
{
|
||||
"name": "vue-meta",
|
||||
"description": "manage page meta info in Vue 2.0 server-rendered apps",
|
||||
"version": "1.6.0",
|
||||
"author": "Declan de Wet <declandewet@me.com>",
|
||||
"bugs": "https://github.com/declandewet/vue-meta/issues",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"codecov": "codecov",
|
||||
"deploy": "npm version",
|
||||
"dev": "babel-node examples/server.js",
|
||||
"lint": "standard --verbose | snazzy",
|
||||
"minify": "uglifyjs lib/vue-meta.js -cm --comments -o lib/vue-meta.min.js",
|
||||
"postbuild": "npm run minify",
|
||||
"postdeploy": "git push origin master --follow-tags && npm run release",
|
||||
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\"",
|
||||
"prebuild": "rimraf lib",
|
||||
"predeploy": "git checkout master && git pull -r",
|
||||
"prerelease": "npm run build",
|
||||
"pretest": "npm run lint",
|
||||
"preversion": "npm run toc",
|
||||
"release": "npm publish",
|
||||
"test": "cross-env NODE_ENV=test karma start karma.conf.js",
|
||||
"toc": "doctoc README.md --title '# Table of Contents'",
|
||||
"update-cdn": "babel-node scripts/update-cdn.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^3.2.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.uniqueid": "^4.0.1",
|
||||
"object-assign": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.2.3",
|
||||
"@babel/core": "^7.4.0",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@babel/preset-env": "^7.4.2",
|
||||
"@babel/register": "^7.4.0",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-istanbul": "^5.1.1",
|
||||
"chai": "^4.2.0",
|
||||
"codecov": "^3.2.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"doctoc": "^1.4.0",
|
||||
"es6-promise": "^4.2.6",
|
||||
"express": "^4.16.4",
|
||||
"express-urlrewrite": "^1.2.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"karma": "^4.0.1",
|
||||
"karma-chai": "^0.1.0",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^3.0.5",
|
||||
"mocha": "^6.0.2",
|
||||
"phantomjs-prebuilt": "^2.1.16",
|
||||
"rimraf": "^2.6.3",
|
||||
"rollup": "^1.7.4",
|
||||
"rollup-plugin-buble": "^0.19.6",
|
||||
"rollup-plugin-commonjs": "^9.2.2",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.1",
|
||||
"snazzy": "^8.0.0",
|
||||
"standard": "^12.0.1",
|
||||
"uglify-js": "^3.5.2",
|
||||
"update-section": "^0.3.3",
|
||||
"vue": "^2.6.10",
|
||||
"vue-loader": "^15.7.0",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-server-renderer": "^2.6.10",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vuex": "^3.1.0",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-dev-server": "^3.2.1"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"types/index.d.ts",
|
||||
"types/vue.d.ts"
|
||||
],
|
||||
"homepage": "https://github.com/declandewet/vue-meta",
|
||||
"description": "Manage page metadata in Vue.js components with ssr support",
|
||||
"keywords": [
|
||||
"attribute",
|
||||
"google",
|
||||
"head",
|
||||
"helmet",
|
||||
"info",
|
||||
"metadata",
|
||||
"meta",
|
||||
"seo",
|
||||
"server",
|
||||
@@ -96,29 +17,111 @@
|
||||
"universal",
|
||||
"vue"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "lib/vue-meta.js",
|
||||
"typings": "types/index.d.ts",
|
||||
"nyc": {
|
||||
"exclude": [
|
||||
"test/**/*.js"
|
||||
]
|
||||
},
|
||||
"homepage": "https://github.com/nuxt/vue-meta",
|
||||
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
||||
"repository": {
|
||||
"url": "git@github.com:declandewet/vue-meta.git",
|
||||
"type": "git"
|
||||
"type": "git",
|
||||
"url": "git@github.com/nuxt/vue-meta.git"
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"Vue",
|
||||
"define",
|
||||
"describe",
|
||||
"it",
|
||||
"expect",
|
||||
"before",
|
||||
"beforeEach",
|
||||
"after",
|
||||
"afterEach"
|
||||
]
|
||||
"license": "MIT",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Declan de Wet (@declandewet)"
|
||||
},
|
||||
{
|
||||
"name": "Sebastien Chopin (@Atinux)"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"lib",
|
||||
"es",
|
||||
"types/*.d.ts"
|
||||
],
|
||||
"main": "lib/vue-meta.common.js",
|
||||
"web": "lib/vue-meta.js",
|
||||
"module": "es/index.js",
|
||||
"typings": "types/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "yarn build:other && yarn build:es",
|
||||
"build:es": "rimraf es && babel src --env-name es --out-dir es",
|
||||
"build:other": "rimraf lib && rollup -c scripts/rollup.config.js",
|
||||
"coverage": "codecov",
|
||||
"dev": "cd examples && yarn dev && cd ..",
|
||||
"docs": "vuepress dev --host 0.0.0.0 --port 3000 docs",
|
||||
"docs:build": "vuepress build docs",
|
||||
"lint": "eslint src test",
|
||||
"prerelease": "git checkout master && git pull -r",
|
||||
"release": "yarn lint && yarn test && yarn build && standard-version",
|
||||
"postrelease": "git push origin master --follow-tags && yarn publish",
|
||||
"test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser",
|
||||
"test:e2e-ssr": "jest test/e2e/ssr",
|
||||
"test:e2e-browser": "jest test/e2e/browser",
|
||||
"test:unit": "jest test/unit",
|
||||
"test:types": "tsc -p types/test"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^3.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"webpack-dev-middleware": "3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.3",
|
||||
"@babel/core": "^7.4.3",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.4.3",
|
||||
"@nuxt/babel-preset-app": "^2.6.2",
|
||||
"@nuxtjs/eslint-config": "^0.0.1",
|
||||
"@vue/server-test-utils": "^1.0.0-beta.29",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.7.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||
"browserstack-local": "^1.3.7",
|
||||
"chromedriver": "^73.0.0",
|
||||
"codecov": "^3.3.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.17.2",
|
||||
"eslint-plugin-jest": "^22.4.1",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"esm": "^3.2.22",
|
||||
"fs-extra": "^7.0.1",
|
||||
"geckodriver": "^1.16.2",
|
||||
"is-wsl": "^1.1.0",
|
||||
"jest": "^24.7.1",
|
||||
"jest-environment-jsdom": "^24.7.1",
|
||||
"jest-environment-jsdom-global": "^1.2.0",
|
||||
"jsdom": "^14.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-env-file": "^0.1.8",
|
||||
"puppeteer-core": "^1.14.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"rollup": "^1.10.1",
|
||||
"rollup-plugin-buble": "^0.19.6",
|
||||
"rollup-plugin-commonjs": "^9.3.4",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-node-resolve": "^4.2.3",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"selenium-webdriver": "^4.0.0-alpha.1",
|
||||
"standard-version": "^5.0.2",
|
||||
"tib": "^0.5.1",
|
||||
"typescript": "^3.4.4",
|
||||
"vue": "^2.6.10",
|
||||
"vue-jest": "^3.0.4",
|
||||
"vue-loader": "^15.7.0",
|
||||
"vue-router": "^3.0.6",
|
||||
"vue-server-renderer": "^2.6.10",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vuepress": "^0.14.11",
|
||||
"vuepress-theme-vue": "^1.1.0",
|
||||
"webpack": "^4.30.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import json from 'rollup-plugin-json'
|
||||
import buble from 'rollup-plugin-buble'
|
||||
|
||||
const pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: './src/index.js',
|
||||
output: {
|
||||
file: pkg.main,
|
||||
format: 'umd',
|
||||
name: 'VueMeta',
|
||||
banner: `/**
|
||||
* vue-meta v${pkg.version}
|
||||
* (c) ${new Date().getFullYear()} Declan de Wet & Sébastien Chopin (@Atinux)
|
||||
* @license MIT
|
||||
*/
|
||||
`.replace(/ {4}/gm, '').trim()
|
||||
},
|
||||
plugins: [
|
||||
json(),
|
||||
nodeResolve({ jsnext: true }),
|
||||
commonjs(),
|
||||
buble()
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import json from 'rollup-plugin-json'
|
||||
import buble from 'rollup-plugin-buble'
|
||||
import replace from 'rollup-plugin-replace'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import defaultsDeep from 'lodash/defaultsDeep'
|
||||
|
||||
const pkg = require('../package.json')
|
||||
|
||||
const banner = `/**
|
||||
* vue-meta v${pkg.version}
|
||||
* (c) ${new Date().getFullYear()}
|
||||
* - Declan de Wet
|
||||
* - Sébastien Chopin (@Atinux)
|
||||
* - All the amazing contributors
|
||||
* @license MIT
|
||||
*/
|
||||
`
|
||||
|
||||
function rollupConfig({
|
||||
plugins = [],
|
||||
...config
|
||||
}) {
|
||||
|
||||
const replaceConfig = {
|
||||
exclude: 'node_modules/**',
|
||||
delimiters: ['', ''],
|
||||
values: {
|
||||
// replaceConfig needs to have some values
|
||||
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = false',
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.output.format || config.output.format === 'umd') {
|
||||
replaceConfig.values = {
|
||||
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true',
|
||||
}
|
||||
}
|
||||
|
||||
return defaultsDeep({}, config, {
|
||||
input: 'src/browser.js',
|
||||
output: {
|
||||
name: 'VueMeta',
|
||||
format: 'umd',
|
||||
sourcemap: false,
|
||||
banner
|
||||
},
|
||||
plugins: [
|
||||
json(),
|
||||
nodeResolve(),
|
||||
replace(replaceConfig)
|
||||
].concat(plugins),
|
||||
})
|
||||
}
|
||||
|
||||
export default [
|
||||
rollupConfig({
|
||||
output: {
|
||||
file: pkg.web,
|
||||
},
|
||||
plugins: [
|
||||
commonjs(),
|
||||
buble()
|
||||
]
|
||||
}),
|
||||
rollupConfig({
|
||||
output: {
|
||||
file: pkg.web.replace('.js', '.min.js'),
|
||||
},
|
||||
plugins: [
|
||||
commonjs(),
|
||||
buble(),
|
||||
terser()
|
||||
]
|
||||
}),
|
||||
rollupConfig({
|
||||
input: 'src/index.js',
|
||||
output: {
|
||||
file: pkg.main,
|
||||
format: 'cjs'
|
||||
},
|
||||
plugins: [
|
||||
commonjs()
|
||||
],
|
||||
external: Object.keys(pkg.dependencies)
|
||||
})
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
import { version } from '../package.json'
|
||||
import createMixin from './shared/mixin'
|
||||
import { setOptions } from './shared/options'
|
||||
import { isUndefined } from './utils/is-type'
|
||||
import $meta from './client/$meta'
|
||||
import { hasMetaInfo } from './shared/meta-helpers'
|
||||
|
||||
/**
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
function install(Vue, options = {}) {
|
||||
options = setOptions(options)
|
||||
|
||||
Vue.prototype.$meta = $meta(options)
|
||||
|
||||
Vue.mixin(createMixin(Vue, options))
|
||||
}
|
||||
|
||||
// automatic install
|
||||
if (!isUndefined(window) && !isUndefined(window.Vue)) {
|
||||
/* istanbul ignore next */
|
||||
install(window.Vue)
|
||||
}
|
||||
|
||||
export default {
|
||||
version,
|
||||
install,
|
||||
hasMetaInfo
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getOptions } from '../shared/options'
|
||||
import { pause, resume } from '../shared/pausing'
|
||||
import refresh from './refresh'
|
||||
|
||||
export default function _$meta(options = {}) {
|
||||
const _refresh = refresh(options)
|
||||
const inject = () => {}
|
||||
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
* @return {Object} - injector
|
||||
*/
|
||||
return function $meta() {
|
||||
return {
|
||||
getOptions: () => getOptions(options),
|
||||
refresh: _refresh.bind(this),
|
||||
inject,
|
||||
pause: pause.bind(this),
|
||||
resume: resume.bind(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { hasGlobalWindow } from '../utils/window'
|
||||
|
||||
// fallback to timers if rAF not present
|
||||
const stopUpdate = (typeof window !== 'undefined' ? window.cancelAnimationFrame : null) || clearTimeout
|
||||
const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFrame : null) || ((cb) => setTimeout(cb, 0))
|
||||
const stopUpdate = (hasGlobalWindow ? window.cancelAnimationFrame : null) || clearTimeout
|
||||
const startUpdate = (hasGlobalWindow ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
|
||||
|
||||
/**
|
||||
* Performs a batched update. Uses requestAnimationFrame to prevent
|
||||
@@ -12,8 +14,9 @@ const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFram
|
||||
* @param {Function} callback - the update to perform
|
||||
* @return {Number} id - a new ID
|
||||
*/
|
||||
export default function batchUpdate (id, callback) {
|
||||
export default function batchUpdate(id, callback) {
|
||||
stopUpdate(id)
|
||||
|
||||
return startUpdate(() => {
|
||||
id = null
|
||||
callback()
|
||||
|
||||
+13
-5
@@ -1,7 +1,9 @@
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import { isFunction } from '../utils/is-type'
|
||||
import { clientSequences } from '../shared/escaping'
|
||||
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||
|
||||
export default function _refresh (options = {}) {
|
||||
export default function _refresh(options = {}) {
|
||||
/**
|
||||
* When called, will update the current meta info with new meta info.
|
||||
* Useful when updating meta info as the result of an asynchronous
|
||||
@@ -12,9 +14,15 @@ export default function _refresh (options = {}) {
|
||||
*
|
||||
* @return {Object} - new meta info
|
||||
*/
|
||||
return function refresh () {
|
||||
const info = getMetaInfo(options)(this.$root)
|
||||
updateClientMetaInfo(options).call(this, info)
|
||||
return info
|
||||
return function refresh() {
|
||||
const metaInfo = getMetaInfo(options, this.$root, clientSequences)
|
||||
|
||||
const tags = updateClientMetaInfo(options, metaInfo)
|
||||
// emit "event" with new info
|
||||
if (tags && isFunction(metaInfo.changed)) {
|
||||
metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags)
|
||||
}
|
||||
|
||||
return { vm: this, metaInfo, tags }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import batchUpdate from './batchUpdate'
|
||||
|
||||
// store an id to keep track of DOM updates
|
||||
let batchId = null
|
||||
|
||||
export default function triggerUpdate(vm, hookName) {
|
||||
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchId = batchUpdate(batchId, () => {
|
||||
vm.$meta().refresh()
|
||||
batchId = null
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,76 @@
|
||||
import updateTitle from './updaters/updateTitle'
|
||||
import updateTagAttributes from './updaters/updateTagAttributes'
|
||||
import updateTags from './updaters/updateTags'
|
||||
import { metaInfoOptionKeys, metaInfoAttributeKeys } from '../shared/constants'
|
||||
import { isArray } from '../utils/is-type'
|
||||
import { includes } from '../utils/array'
|
||||
import { updateAttribute, updateTag, updateTitle } from './updaters'
|
||||
|
||||
export default function _updateClientMetaInfo (options = {}) {
|
||||
function getTag(tags, tag) {
|
||||
if (!tags[tag]) {
|
||||
tags[tag] = document.getElementsByTagName(tag)[0]
|
||||
}
|
||||
|
||||
return tags[tag]
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs client-side updates when new meta info is received
|
||||
*
|
||||
* @param {Object} newInfo - the meta info to update to
|
||||
*/
|
||||
export default function updateClientMetaInfo(options = {}, newInfo) {
|
||||
const { ssrAttribute } = options
|
||||
|
||||
/**
|
||||
* Performs client-side updates when new meta info is received
|
||||
*
|
||||
* @param {Object} newInfo - the meta info to update to
|
||||
*/
|
||||
return function updateClientMetaInfo (newInfo) {
|
||||
const htmlTag = document.getElementsByTagName('html')[0]
|
||||
// if this is not a server render, then update
|
||||
if (htmlTag.getAttribute(ssrAttribute) === null) {
|
||||
// initialize tracked changes
|
||||
const addedTags = {}
|
||||
const removedTags = {}
|
||||
// only cache tags for current update
|
||||
const tags = {}
|
||||
|
||||
Object.keys(newInfo).forEach((key) => {
|
||||
switch (key) {
|
||||
// update the title
|
||||
case 'title':
|
||||
updateTitle(options)(newInfo.title)
|
||||
break
|
||||
// update attributes
|
||||
case 'htmlAttrs':
|
||||
updateTagAttributes(options)(newInfo[key], htmlTag)
|
||||
break
|
||||
case 'bodyAttrs':
|
||||
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('body')[0])
|
||||
break
|
||||
case 'headAttrs':
|
||||
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('head')[0])
|
||||
break
|
||||
// ignore these
|
||||
case 'titleChunk':
|
||||
case 'titleTemplate':
|
||||
case 'changed':
|
||||
case '__dangerouslyDisableSanitizers':
|
||||
break
|
||||
// catch-all update tags
|
||||
default:
|
||||
const headTag = document.getElementsByTagName('head')[0]
|
||||
const bodyTag = document.getElementsByTagName('body')[0]
|
||||
const { oldTags, newTags } = updateTags(options)(key, newInfo[key], headTag, bodyTag)
|
||||
if (newTags.length) {
|
||||
addedTags[key] = newTags
|
||||
removedTags[key] = oldTags
|
||||
}
|
||||
}
|
||||
})
|
||||
const htmlTag = getTag(tags, 'html')
|
||||
|
||||
// emit "event" with new info
|
||||
if (typeof newInfo.changed === 'function') {
|
||||
newInfo.changed.call(this, newInfo, addedTags, removedTags)
|
||||
}
|
||||
} else {
|
||||
// remove the server render attribute so we can update on changes
|
||||
htmlTag.removeAttribute(ssrAttribute)
|
||||
// if this is a server render, then dont update
|
||||
if (htmlTag.hasAttribute(ssrAttribute)) {
|
||||
// remove the server render attribute so we can update on (next) changes
|
||||
htmlTag.removeAttribute(ssrAttribute)
|
||||
return false
|
||||
}
|
||||
|
||||
// initialize tracked changes
|
||||
const addedTags = {}
|
||||
const removedTags = {}
|
||||
|
||||
for (const type in newInfo) {
|
||||
// ignore these
|
||||
if (includes(metaInfoOptionKeys, type)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (type === 'title') {
|
||||
// update the title
|
||||
updateTitle(newInfo.title)
|
||||
continue
|
||||
}
|
||||
|
||||
if (includes(metaInfoAttributeKeys, type)) {
|
||||
const tagName = type.substr(0, 4)
|
||||
updateAttribute(options, newInfo[type], getTag(tags, tagName))
|
||||
continue
|
||||
}
|
||||
|
||||
// tags should always be an array, ignore if it isnt
|
||||
if (!isArray(newInfo[type])) {
|
||||
continue
|
||||
}
|
||||
|
||||
const { oldTags, newTags } = updateTag(
|
||||
options,
|
||||
type,
|
||||
newInfo[type],
|
||||
getTag(tags, 'head'),
|
||||
getTag(tags, 'body')
|
||||
)
|
||||
|
||||
if (newTags.length) {
|
||||
addedTags[type] = newTags
|
||||
removedTags[type] = oldTags
|
||||
}
|
||||
}
|
||||
|
||||
return { addedTags, removedTags }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { booleanHtmlAttributes } from '../../shared/constants'
|
||||
import { toArray, includes } from '../../utils/array'
|
||||
import { isArray } from '../../utils/is-type'
|
||||
|
||||
/**
|
||||
* Updates the document's html tag attributes
|
||||
*
|
||||
* @param {Object} attrs - the new document html attributes
|
||||
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
|
||||
*/
|
||||
export default function updateAttribute({ attribute } = {}, attrs, tag) {
|
||||
const vueMetaAttrString = tag.getAttribute(attribute)
|
||||
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
||||
const toRemove = toArray(vueMetaAttrs)
|
||||
|
||||
const keepIndexes = []
|
||||
for (const attr in attrs) {
|
||||
if (attrs.hasOwnProperty(attr)) {
|
||||
const value = includes(booleanHtmlAttributes, attr)
|
||||
? ''
|
||||
: isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr]
|
||||
|
||||
tag.setAttribute(attr, value || '')
|
||||
|
||||
if (!includes(vueMetaAttrs, attr)) {
|
||||
vueMetaAttrs.push(attr)
|
||||
}
|
||||
|
||||
// filter below wont ever check -1
|
||||
keepIndexes.push(toRemove.indexOf(attr))
|
||||
}
|
||||
}
|
||||
|
||||
const removedAttributesCount = toRemove
|
||||
.filter((el, index) => !includes(keepIndexes, index))
|
||||
.reduce((acc, attr) => {
|
||||
tag.removeAttribute(attr)
|
||||
return acc + 1
|
||||
}, 0)
|
||||
|
||||
if (vueMetaAttrs.length === removedAttributesCount) {
|
||||
tag.removeAttribute(attribute)
|
||||
} else {
|
||||
tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(','))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as updateAttribute } from './attribute'
|
||||
export { default as updateTitle } from './title'
|
||||
export { default as updateTag } from './tag'
|
||||
@@ -0,0 +1,85 @@
|
||||
import { isUndefined } from '../../utils/is-type'
|
||||
import { toArray, includes } from '../../utils/array'
|
||||
|
||||
/**
|
||||
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
|
||||
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - a representation of what tags changed
|
||||
*/
|
||||
export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) {
|
||||
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
|
||||
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
|
||||
const dataAttributes = [tagIDKeyName, 'body']
|
||||
const newTags = []
|
||||
|
||||
if (tags.length > 1) {
|
||||
// remove duplicates that could have been found by merging tags
|
||||
// which include a mixin with metaInfo and that mixin is used
|
||||
// by multiple components on the same page
|
||||
const found = []
|
||||
tags = tags.filter((x) => {
|
||||
const k = JSON.stringify(x)
|
||||
const res = !includes(found, k)
|
||||
found.push(k)
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (tags.length) {
|
||||
tags.forEach((tag) => {
|
||||
const newElement = document.createElement(type)
|
||||
newElement.setAttribute(attribute, 'true')
|
||||
|
||||
const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags
|
||||
|
||||
for (const attr in tag) {
|
||||
if (tag.hasOwnProperty(attr)) {
|
||||
if (attr === 'innerHTML') {
|
||||
newElement.innerHTML = tag.innerHTML
|
||||
} else if (attr === 'cssText') {
|
||||
if (newElement.styleSheet) {
|
||||
/* istanbul ignore next */
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
} else {
|
||||
const _attr = includes(dataAttributes, attr)
|
||||
? `data-${attr}`
|
||||
: attr
|
||||
const value = isUndefined(tag[attr]) ? '' : tag[attr]
|
||||
newElement.setAttribute(_attr, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
|
||||
let indexToDelete
|
||||
const hasEqualElement = oldTags.some((existingTag, index) => {
|
||||
indexToDelete = index
|
||||
return newElement.isEqualNode(existingTag)
|
||||
})
|
||||
|
||||
if (hasEqualElement && (indexToDelete || indexToDelete === 0)) {
|
||||
oldTags.splice(indexToDelete, 1)
|
||||
} else {
|
||||
newTags.push(newElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const oldTags = oldHeadTags.concat(oldBodyTags)
|
||||
oldTags.forEach(tag => tag.parentNode.removeChild(tag))
|
||||
newTags.forEach((tag) => {
|
||||
if (tag.getAttribute('data-body') === 'true') {
|
||||
bodyTag.appendChild(tag)
|
||||
} else {
|
||||
headTag.appendChild(tag)
|
||||
}
|
||||
})
|
||||
|
||||
return { oldTags, newTags }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Updates the document title
|
||||
*
|
||||
* @param {String} title - the new title of the document
|
||||
*/
|
||||
export default function updateTitle(title = document.title) {
|
||||
document.title = title
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export default function _updateTagAttributes (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Updates the document's html tag attributes
|
||||
*
|
||||
* @param {Object} attrs - the new document html attributes
|
||||
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
|
||||
*/
|
||||
return function updateTagAttributes (attrs, tag) {
|
||||
const vueMetaAttrString = tag.getAttribute(attribute)
|
||||
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
||||
const toRemove = [].concat(vueMetaAttrs)
|
||||
for (let attr in attrs) {
|
||||
if (attrs.hasOwnProperty(attr)) {
|
||||
const val = attrs[attr] || ''
|
||||
tag.setAttribute(attr, val)
|
||||
if (vueMetaAttrs.indexOf(attr) === -1) {
|
||||
vueMetaAttrs.push(attr)
|
||||
}
|
||||
const saveIndex = toRemove.indexOf(attr)
|
||||
if (saveIndex !== -1) {
|
||||
toRemove.splice(saveIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = toRemove.length - 1
|
||||
for (; i >= 0; i--) {
|
||||
tag.removeAttribute(toRemove[i])
|
||||
}
|
||||
if (vueMetaAttrs.length === toRemove.length) {
|
||||
tag.removeAttribute(attribute)
|
||||
} else {
|
||||
tag.setAttribute(attribute, vueMetaAttrs.join(','))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// borrow the slice method
|
||||
const toArray = Function.prototype.call.bind(Array.prototype.slice)
|
||||
|
||||
export default function _updateTags (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
|
||||
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - a representation of what tags changed
|
||||
*/
|
||||
return function updateTags (type, tags, headTag, bodyTag) {
|
||||
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
|
||||
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
|
||||
const newTags = []
|
||||
let indexToDelete
|
||||
|
||||
if (tags.length > 1) {
|
||||
// remove duplicates that could have been found by merging tags
|
||||
// which include a mixin with metaInfo and that mixin is used
|
||||
// by multiple components on the same page
|
||||
const found = []
|
||||
tags = tags.map(x => {
|
||||
const k = JSON.stringify(x)
|
||||
if (found.indexOf(k) < 0) {
|
||||
found.push(k)
|
||||
return x
|
||||
}
|
||||
}).filter(x => x)
|
||||
}
|
||||
|
||||
if (tags && tags.length) {
|
||||
tags.forEach((tag) => {
|
||||
const newElement = document.createElement(type)
|
||||
const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags
|
||||
|
||||
for (const attr in tag) {
|
||||
if (tag.hasOwnProperty(attr)) {
|
||||
if (attr === 'innerHTML') {
|
||||
newElement.innerHTML = tag.innerHTML
|
||||
} else if (attr === 'cssText') {
|
||||
if (newElement.styleSheet) {
|
||||
newElement.styleSheet.cssText = tag.cssText
|
||||
} else {
|
||||
newElement.appendChild(document.createTextNode(tag.cssText))
|
||||
}
|
||||
} else if ([options.tagIDKeyName, 'body'].indexOf(attr) !== -1) {
|
||||
const _attr = `data-${attr}`
|
||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||
newElement.setAttribute(_attr, value)
|
||||
} else {
|
||||
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
|
||||
newElement.setAttribute(attr, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newElement.setAttribute(attribute, 'true')
|
||||
|
||||
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
|
||||
if (oldTags.some((existingTag, index) => {
|
||||
indexToDelete = index
|
||||
return newElement.isEqualNode(existingTag)
|
||||
})) {
|
||||
oldTags.splice(indexToDelete, 1)
|
||||
} else {
|
||||
newTags.push(newElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
const oldTags = oldHeadTags.concat(oldBodyTags)
|
||||
oldTags.forEach((tag) => tag.parentNode.removeChild(tag))
|
||||
newTags.forEach((tag) => {
|
||||
if (tag.getAttribute('data-body') === 'true') {
|
||||
bodyTag.appendChild(tag)
|
||||
} else {
|
||||
headTag.appendChild(tag)
|
||||
}
|
||||
})
|
||||
|
||||
return { oldTags, newTags }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function _updateTitle () {
|
||||
/**
|
||||
* Updates the document title
|
||||
*
|
||||
* @param {String} title - the new title of the document
|
||||
*/
|
||||
return function updateTitle (title = document.title) {
|
||||
document.title = title
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -1,6 +1,23 @@
|
||||
import install from './shared/plugin'
|
||||
import { version } from '../package.json'
|
||||
import createMixin from './shared/mixin'
|
||||
import { setOptions } from './shared/options'
|
||||
import $meta from './server/$meta'
|
||||
import { hasMetaInfo } from './shared/meta-helpers'
|
||||
|
||||
install.version = version
|
||||
/**
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
function install(Vue, options = {}) {
|
||||
options = setOptions(options)
|
||||
|
||||
export default install
|
||||
Vue.prototype.$meta = $meta(options)
|
||||
|
||||
Vue.mixin(createMixin(Vue, options))
|
||||
}
|
||||
|
||||
export default {
|
||||
version,
|
||||
install,
|
||||
hasMetaInfo
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getOptions } from '../shared/options'
|
||||
import { pause, resume } from '../shared/pausing'
|
||||
import refresh from '../client/refresh'
|
||||
import inject from './inject'
|
||||
|
||||
export default function _$meta(options = {}) {
|
||||
const _refresh = refresh(options)
|
||||
const _inject = inject(options)
|
||||
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
* @return {Object} - injector
|
||||
*/
|
||||
return function $meta() {
|
||||
return {
|
||||
getOptions: () => getOptions(options),
|
||||
refresh: _refresh.bind(this),
|
||||
inject: _inject.bind(this),
|
||||
pause: pause.bind(this),
|
||||
resume: resume.bind(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
import titleGenerator from './generators/titleGenerator'
|
||||
import attrsGenerator from './generators/attrsGenerator'
|
||||
import tagGenerator from './generators/tagGenerator'
|
||||
import { metaInfoAttributeKeys } from '../shared/constants'
|
||||
import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
|
||||
|
||||
export default function _generateServerInjector (options = {}) {
|
||||
/**
|
||||
* Converts a meta info property to one that can be stringified on the server
|
||||
*
|
||||
* @param {String} type - the type of data to convert
|
||||
* @param {(String|Object|Array<Object>)} data - the data value
|
||||
* @return {Object} - the new injector
|
||||
*/
|
||||
return function generateServerInjector (type, data) {
|
||||
switch (type) {
|
||||
case 'title':
|
||||
return titleGenerator(options)(type, data)
|
||||
case 'htmlAttrs':
|
||||
case 'bodyAttrs':
|
||||
case 'headAttrs':
|
||||
return attrsGenerator(options)(type, data)
|
||||
default:
|
||||
return tagGenerator(options)(type, data)
|
||||
}
|
||||
/**
|
||||
* Converts a meta info property to one that can be stringified on the server
|
||||
*
|
||||
* @param {String} type - the type of data to convert
|
||||
* @param {(String|Object|Array<Object>)} data - the data value
|
||||
* @return {Object} - the new injector
|
||||
*/
|
||||
|
||||
export default function generateServerInjector(options, type, data) {
|
||||
if (type === 'title') {
|
||||
return titleGenerator(options, type, data)
|
||||
}
|
||||
|
||||
if (metaInfoAttributeKeys.includes(type)) {
|
||||
return attributeGenerator(options, type, data)
|
||||
}
|
||||
|
||||
return tagGenerator(options, type, data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { booleanHtmlAttributes } from '../../shared/constants'
|
||||
import { isUndefined, isArray } from '../../utils/is-type'
|
||||
|
||||
/**
|
||||
* Generates tag attributes for use on the server.
|
||||
*
|
||||
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
|
||||
* @param {Object} data - the attributes to generate
|
||||
* @return {Object} - the attribute generator
|
||||
*/
|
||||
export default function attributeGenerator({ attribute } = {}, type, data) {
|
||||
return {
|
||||
text() {
|
||||
let attributeStr = ''
|
||||
const watchedAttrs = []
|
||||
|
||||
for (const attr in data) {
|
||||
if (data.hasOwnProperty(attr)) {
|
||||
watchedAttrs.push(attr)
|
||||
|
||||
attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr)
|
||||
? attr
|
||||
: `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"`
|
||||
|
||||
attributeStr += ' '
|
||||
}
|
||||
}
|
||||
|
||||
attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"`
|
||||
return attributeStr
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export default function _attrsGenerator (options = {}) {
|
||||
const { attribute } = options
|
||||
|
||||
/**
|
||||
* Generates tag attributes for use on the server.
|
||||
*
|
||||
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
|
||||
* @param {Object} data - the attributes to generate
|
||||
* @return {Object} - the attribute generator
|
||||
*/
|
||||
return function attrsGenerator (type, data) {
|
||||
return {
|
||||
text () {
|
||||
let attributeStr = ''
|
||||
let watchedAttrs = []
|
||||
for (let attr in data) {
|
||||
if (data.hasOwnProperty(attr)) {
|
||||
watchedAttrs.push(attr)
|
||||
attributeStr += `${
|
||||
typeof data[attr] !== 'undefined'
|
||||
? `${attr}="${data[attr]}"`
|
||||
: attr
|
||||
} `
|
||||
}
|
||||
}
|
||||
attributeStr += `${attribute}="${watchedAttrs.join(',')}"`
|
||||
return attributeStr.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as attributeGenerator } from './attribute'
|
||||
export { default as titleGenerator } from './title'
|
||||
export { default as tagGenerator } from './tag'
|
||||
@@ -0,0 +1,65 @@
|
||||
import { booleanHtmlAttributes, tagsWithoutEndTag, tagsWithInnerContent, tagAttributeAsInnerContent } from '../../shared/constants'
|
||||
import { isUndefined } from '../../utils/is-type'
|
||||
|
||||
/**
|
||||
* Generates meta, base, link, style, script, noscript tags for use on the server
|
||||
*
|
||||
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag
|
||||
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
|
||||
* @return {Object} - the tag generator
|
||||
*/
|
||||
export default function tagGenerator({ attribute, tagIDKeyName } = {}, type, tags) {
|
||||
return {
|
||||
text({ body = false } = {}) {
|
||||
// build a string containing all tags of this type
|
||||
return tags.reduce((tagsStr, tag) => {
|
||||
const tagKeys = Object.keys(tag)
|
||||
|
||||
if (tagKeys.length === 0) {
|
||||
return tagsStr // Bail on empty tag object
|
||||
}
|
||||
|
||||
if (Boolean(tag.body) !== body) {
|
||||
return tagsStr
|
||||
}
|
||||
|
||||
// build a string containing all attributes of this tag
|
||||
const attrs = tagKeys.reduce((attrsStr, attr) => {
|
||||
// these attributes are treated as children on the tag
|
||||
if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') {
|
||||
return attrsStr
|
||||
}
|
||||
|
||||
// these form the attribute list for this tag
|
||||
let prefix = ''
|
||||
if ([tagIDKeyName, 'body'].includes(attr)) {
|
||||
prefix = 'data-'
|
||||
}
|
||||
|
||||
return isUndefined(tag[attr]) || booleanHtmlAttributes.includes(attr)
|
||||
? `${attrsStr} ${prefix}${attr}`
|
||||
: `${attrsStr} ${prefix}${attr}="${tag[attr]}"`
|
||||
}, '')
|
||||
|
||||
// grab child content from one of these attributes, if possible
|
||||
const content = tag.innerHTML || tag.cssText || ''
|
||||
|
||||
// generate tag exactly without any other redundant attribute
|
||||
const observeTag = tag.once
|
||||
? ''
|
||||
: `${attribute}="true"`
|
||||
|
||||
// these tags have no end tag
|
||||
const hasEndTag = !tagsWithoutEndTag.includes(type)
|
||||
|
||||
// these tag types will have content inserted
|
||||
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
|
||||
|
||||
// the final string for this specific tag
|
||||
return !hasContent
|
||||
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
|
||||
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
|
||||
}, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generates title output for the server
|
||||
*
|
||||
* @param {'title'} type - the string "title"
|
||||
* @param {String} data - the title text
|
||||
* @return {Object} - the title generator
|
||||
*/
|
||||
export default function titleGenerator({ attribute } = {}, type, data) {
|
||||
return {
|
||||
text() {
|
||||
return `<${type} ${attribute}="true">${data}</${type}>`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import getMetaInfo from '../shared/getMetaInfo'
|
||||
import { metaInfoOptionKeys } from '../shared/constants'
|
||||
import { serverSequences } from '../shared/escaping'
|
||||
import generateServerInjector from './generateServerInjector'
|
||||
|
||||
export default function _inject (options = {}) {
|
||||
export default function _inject(options = {}) {
|
||||
/**
|
||||
* Converts the state of the meta info object such that each item
|
||||
* can be compiled to a tag string on the server
|
||||
@@ -9,17 +11,17 @@ export default function _inject (options = {}) {
|
||||
* @this {Object} - Vue instance - ideally the root component
|
||||
* @return {Object} - server meta info with `toString` methods
|
||||
*/
|
||||
return function inject () {
|
||||
return function inject() {
|
||||
// get meta info with sensible defaults
|
||||
const info = getMetaInfo(options)(this.$root)
|
||||
const metaInfo = getMetaInfo(options, this.$root, serverSequences)
|
||||
|
||||
// generate server injectors
|
||||
for (let key in info) {
|
||||
if (info.hasOwnProperty(key) && key !== 'titleTemplate' && key !== 'titleChunk') {
|
||||
info[key] = generateServerInjector(options)(key, info[key])
|
||||
for (const key in metaInfo) {
|
||||
if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) {
|
||||
metaInfo[key] = generateServerInjector(options, key, metaInfo[key])
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
return metaInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import inject from '../server/inject'
|
||||
import refresh from '../client/refresh'
|
||||
|
||||
export default function _$meta (options = {}) {
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
* @return {Object} - injector
|
||||
*/
|
||||
return function $meta () {
|
||||
return {
|
||||
inject: inject(options).bind(this),
|
||||
refresh: refresh(options).bind(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
-7
@@ -2,26 +2,131 @@
|
||||
* These are constant variables used throughout the application.
|
||||
*/
|
||||
|
||||
// set some sane defaults
|
||||
export const defaultInfo = {
|
||||
title: '',
|
||||
titleChunk: '',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {},
|
||||
bodyAttrs: {},
|
||||
headAttrs: {},
|
||||
base: [],
|
||||
link: [],
|
||||
meta: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
}
|
||||
|
||||
// This is the name of the component option that contains all the information that
|
||||
// gets converted to the various meta tags & attributes for the page.
|
||||
export const VUE_META_KEY_NAME = 'metaInfo'
|
||||
export const keyName = 'metaInfo'
|
||||
|
||||
// This is the attribute vue-meta augments on elements to know which it should
|
||||
// This is the attribute vue-meta arguments on elements to know which it should
|
||||
// manage and which it should ignore.
|
||||
export const VUE_META_ATTRIBUTE = 'data-vue-meta'
|
||||
export const attribute = 'data-vue-meta'
|
||||
|
||||
// This is the attribute that goes on the `html` tag to inform `vue-meta`
|
||||
// that the server has already generated the meta tags for the initial render.
|
||||
export const VUE_META_SERVER_RENDERED_ATTRIBUTE = 'data-vue-meta-server-rendered'
|
||||
export const ssrAttribute = 'data-vue-meta-server-rendered'
|
||||
|
||||
// This is the property that tells vue-meta to overwrite (instead of append)
|
||||
// an item in a tag list. For example, if you have two `meta` tag list items
|
||||
// that both have `vmid` of "description", then vue-meta will overwrite the
|
||||
// shallowest one with the deepest one.
|
||||
export const VUE_META_TAG_LIST_ID_KEY_NAME = 'vmid'
|
||||
export const tagIDKeyName = 'vmid'
|
||||
|
||||
// This is the key name for possible meta templates
|
||||
export const VUE_META_TEMPLATE_KEY_NAME = 'template'
|
||||
export const metaTemplateKeyName = 'template'
|
||||
|
||||
// This is the key name for the content-holding property
|
||||
export const VUE_META_CONTENT_KEY = 'content'
|
||||
export const contentKeyName = 'content'
|
||||
|
||||
export const defaultOptions = {
|
||||
keyName,
|
||||
attribute,
|
||||
ssrAttribute,
|
||||
tagIDKeyName,
|
||||
contentKeyName,
|
||||
metaTemplateKeyName
|
||||
}
|
||||
|
||||
// List of metaInfo property keys which are configuration options (and dont generate html)
|
||||
export const metaInfoOptionKeys = [
|
||||
'titleChunk',
|
||||
'titleTemplate',
|
||||
'changed',
|
||||
'__dangerouslyDisableSanitizers',
|
||||
'__dangerouslyDisableSanitizersByTagID'
|
||||
]
|
||||
|
||||
// The metaInfo property keys which are used to disable escaping
|
||||
export const disableOptionKeys = [
|
||||
'__dangerouslyDisableSanitizers',
|
||||
'__dangerouslyDisableSanitizersByTagID'
|
||||
]
|
||||
|
||||
// List of metaInfo property keys which only generates attributes and no tags
|
||||
export const metaInfoAttributeKeys = [
|
||||
'htmlAttrs',
|
||||
'headAttrs',
|
||||
'bodyAttrs'
|
||||
]
|
||||
|
||||
// HTML elements which dont have a head tag (shortened to our needs)
|
||||
// see: https://www.w3.org/TR/html52/document-metadata.html
|
||||
export const tagsWithoutEndTag = ['base', 'meta', 'link']
|
||||
|
||||
// HTML elements which can have inner content (shortened to our needs)
|
||||
export const tagsWithInnerContent = ['noscript', 'script', 'style']
|
||||
|
||||
// Attributes which are inserted as childNodes instead of HTMLAttribute
|
||||
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText']
|
||||
|
||||
// from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202
|
||||
export const booleanHtmlAttributes = [
|
||||
'allowfullscreen',
|
||||
'amp',
|
||||
'async',
|
||||
'autofocus',
|
||||
'autoplay',
|
||||
'checked',
|
||||
'compact',
|
||||
'controls',
|
||||
'declare',
|
||||
'default',
|
||||
'defaultchecked',
|
||||
'defaultmuted',
|
||||
'defaultselected',
|
||||
'defer',
|
||||
'disabled',
|
||||
'enabled',
|
||||
'formnovalidate',
|
||||
'hidden',
|
||||
'indeterminate',
|
||||
'inert',
|
||||
'ismap',
|
||||
'itemscope',
|
||||
'loop',
|
||||
'multiple',
|
||||
'muted',
|
||||
'nohref',
|
||||
'noresize',
|
||||
'noshade',
|
||||
'novalidate',
|
||||
'nowrap',
|
||||
'open',
|
||||
'pauseonexit',
|
||||
'readonly',
|
||||
'required',
|
||||
'reversed',
|
||||
'scoped',
|
||||
'seamless',
|
||||
'selected',
|
||||
'sortable',
|
||||
'truespeed',
|
||||
'typemustmatch',
|
||||
'visible'
|
||||
]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { isString, isArray, isObject } from '../utils/is-type'
|
||||
import { includes } from '../utils/array'
|
||||
import { metaInfoOptionKeys, disableOptionKeys } from './constants'
|
||||
|
||||
export const serverSequences = [
|
||||
[/&/g, '&'],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, ''']
|
||||
]
|
||||
|
||||
export const clientSequences = [
|
||||
[/&/g, '\u0026'],
|
||||
[/</g, '\u003c'],
|
||||
[/>/g, '\u003e'],
|
||||
[/"/g, '\u0022'],
|
||||
[/'/g, '\u0027']
|
||||
]
|
||||
|
||||
// sanitizes potentially dangerous characters
|
||||
export function escape(info, options, escapeOptions) {
|
||||
const { tagIDKeyName } = options
|
||||
const { doEscape = v => v } = escapeOptions
|
||||
const escaped = {}
|
||||
|
||||
for (const key in info) {
|
||||
const value = info[key]
|
||||
|
||||
// no need to escape configuration options
|
||||
if (includes(metaInfoOptionKeys, key)) {
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
let [ disableKey ] = disableOptionKeys
|
||||
if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) {
|
||||
// this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
const tagId = info[tagIDKeyName]
|
||||
if (tagId) {
|
||||
disableKey = disableOptionKeys[1]
|
||||
|
||||
// keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped
|
||||
if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) {
|
||||
escaped[key] = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(value)) {
|
||||
escaped[key] = doEscape(value)
|
||||
} else if (isArray(value)) {
|
||||
escaped[key] = value.map((v) => {
|
||||
return isObject(v)
|
||||
? escape(v, options, escapeOptions)
|
||||
: doEscape(v)
|
||||
})
|
||||
} else if (isObject(value)) {
|
||||
escaped[key] = escape(value, options, escapeOptions)
|
||||
} else {
|
||||
escaped[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return escaped
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import uniqBy from './uniqBy'
|
||||
import uniqueId from 'lodash.uniqueid'
|
||||
import { isFunction, isObject } from '../utils/is-type'
|
||||
import { findIndex } from '../utils/array'
|
||||
import { merge } from './merge'
|
||||
import { applyTemplate } from './template'
|
||||
import { inMetaInfoBranch } from './meta-helpers'
|
||||
|
||||
/**
|
||||
* Returns the `opts.option` $option value of the given `opts.component`.
|
||||
@@ -10,66 +12,65 @@ import uniqueId from 'lodash.uniqueid'
|
||||
*
|
||||
* @param {Object} opts - options
|
||||
* @param {Object} opts.component - Vue component to fetch option data from
|
||||
* @param {String} opts.option - what option to look for
|
||||
* @param {Boolean} opts.deep - look for data in child components as well?
|
||||
* @param {Function} opts.arrayMerge - how should arrays be merged?
|
||||
* @param {String} opts.keyName - the name of the option to look for
|
||||
* @param {Object} [result={}] - result so far
|
||||
* @return {Object} result - final aggregated result
|
||||
*/
|
||||
export default function getComponentOption (opts, result = {}) {
|
||||
const { component, option, deep, arrayMerge, metaTemplateKeyName, tagIDKeyName, contentKeyName } = opts
|
||||
const { $options } = component
|
||||
export default function getComponentOption(options = {}, component, result = {}) {
|
||||
const { keyName, metaTemplateKeyName, tagIDKeyName } = options
|
||||
const { $options, $children } = component
|
||||
|
||||
if (component._inactive) return result
|
||||
if (component._inactive) {
|
||||
return result
|
||||
}
|
||||
|
||||
// only collect option data if it exists
|
||||
if (typeof $options[option] !== 'undefined' && $options[option] !== null) {
|
||||
let data = $options[option]
|
||||
if ($options[keyName]) {
|
||||
let data = $options[keyName]
|
||||
|
||||
// if option is a function, replace it with it's result
|
||||
if (typeof data === 'function') {
|
||||
if (isFunction(data)) {
|
||||
data = data.call(component)
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
// merge with existing options
|
||||
result = deepmerge(result, data, { arrayMerge })
|
||||
} else {
|
||||
result = data
|
||||
// ignore data if its not an object, then we keep our previous result
|
||||
if (!isObject(data)) {
|
||||
return result
|
||||
}
|
||||
|
||||
// merge with existing options
|
||||
result = merge(result, data, options)
|
||||
}
|
||||
|
||||
// collect & aggregate child options if deep = true
|
||||
if (deep && component.$children.length) {
|
||||
component.$children.forEach((childComponent) => {
|
||||
result = getComponentOption({
|
||||
component: childComponent,
|
||||
option,
|
||||
deep,
|
||||
arrayMerge
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
if (metaTemplateKeyName && result.hasOwnProperty('meta')) {
|
||||
result.meta = Object.keys(result.meta).map(metaKey => {
|
||||
const metaObject = result.meta[metaKey]
|
||||
if (!metaObject.hasOwnProperty(metaTemplateKeyName) || !metaObject.hasOwnProperty(contentKeyName) || typeof metaObject[metaTemplateKeyName] === 'undefined') {
|
||||
return result.meta[metaKey]
|
||||
if ($children.length) {
|
||||
$children.forEach((childComponent) => {
|
||||
// check if the childComponent is in a branch
|
||||
// return otherwise so we dont walk all component branches unnecessarily
|
||||
if (!inMetaInfoBranch(childComponent)) {
|
||||
return
|
||||
}
|
||||
|
||||
const template = metaObject[metaTemplateKeyName]
|
||||
delete metaObject[metaTemplateKeyName]
|
||||
|
||||
if (template) {
|
||||
metaObject.content = typeof template === 'function' ? template(metaObject.content) : template.replace(/%s/g, metaObject.content)
|
||||
}
|
||||
|
||||
return metaObject
|
||||
result = getComponentOption(options, childComponent, result)
|
||||
})
|
||||
result.meta = uniqBy(
|
||||
result.meta,
|
||||
metaObject => metaObject.hasOwnProperty(tagIDKeyName) ? metaObject[tagIDKeyName] : uniqueId()
|
||||
)
|
||||
}
|
||||
|
||||
if (metaTemplateKeyName && result.meta) {
|
||||
// apply templates if needed
|
||||
result.meta.forEach(metaObject => applyTemplate(options, metaObject))
|
||||
|
||||
// remove meta items with duplicate vmid's
|
||||
result.meta = result.meta.filter((metaItem, index, arr) => {
|
||||
return (
|
||||
// keep meta item if it doesnt has a vmid
|
||||
!metaItem.hasOwnProperty(tagIDKeyName) ||
|
||||
// or if it's the first item in the array with this vmid
|
||||
index === findIndex(arr, item => item[tagIDKeyName] === metaItem[tagIDKeyName])
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
+50
-150
@@ -1,156 +1,56 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import isPlainObject from 'lodash.isplainobject'
|
||||
import isArray from './isArray'
|
||||
import { ensureIsArray } from '../utils/ensure'
|
||||
import { applyTemplate } from './template'
|
||||
import { defaultInfo, disableOptionKeys } from './constants'
|
||||
import { escape } from './escaping'
|
||||
import getComponentOption from './getComponentOption'
|
||||
|
||||
const escapeHTML = (str) => typeof window === 'undefined'
|
||||
// server-side escape sequence
|
||||
? String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
// client-side escape sequence
|
||||
: String(str)
|
||||
.replace(/&/g, '\u0026')
|
||||
.replace(/</g, '\u003c')
|
||||
.replace(/>/g, '\u003e')
|
||||
.replace(/"/g, '\u0022')
|
||||
.replace(/'/g, '\u0027')
|
||||
/**
|
||||
* Returns the correct meta info for the given component
|
||||
* (child components will overwrite parent meta info)
|
||||
*
|
||||
* @param {Object} component - the Vue instance to get meta info from
|
||||
* @return {Object} - returned meta info
|
||||
*/
|
||||
export default function getMetaInfo(options = {}, component, escapeSequences = []) {
|
||||
// collect & aggregate all metaInfo $options
|
||||
let info = getComponentOption(options, component, defaultInfo)
|
||||
|
||||
export default function _getMetaInfo (options = {}) {
|
||||
const { keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = options
|
||||
/**
|
||||
* Returns the correct meta info for the given component
|
||||
* (child components will overwrite parent meta info)
|
||||
*
|
||||
* @param {Object} component - the Vue instance to get meta info from
|
||||
* @return {Object} - returned meta info
|
||||
*/
|
||||
return function getMetaInfo (component) {
|
||||
// set some sane defaults
|
||||
const defaultInfo = {
|
||||
title: '',
|
||||
titleChunk: '',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {},
|
||||
bodyAttrs: {},
|
||||
headAttrs: {},
|
||||
meta: [],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: [],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
}
|
||||
// Remove all "template" tags from meta
|
||||
|
||||
// collect & aggregate all metaInfo $options
|
||||
let info = getComponentOption({
|
||||
component,
|
||||
option: keyName,
|
||||
deep: true,
|
||||
metaTemplateKeyName,
|
||||
tagIDKeyName,
|
||||
contentKeyName,
|
||||
arrayMerge (target, source) {
|
||||
// we concat the arrays without merging objects contained in,
|
||||
// but we check for a `vmid` property on each object in the array
|
||||
// using an O(1) lookup associative array exploit
|
||||
// note the use of "for in" - we are looping through arrays here, not
|
||||
// plain objects
|
||||
const destination = []
|
||||
for (let targetIndex in target) {
|
||||
const targetItem = target[targetIndex]
|
||||
let shared = false
|
||||
for (let sourceIndex in source) {
|
||||
const sourceItem = source[sourceIndex]
|
||||
if (targetItem[tagIDKeyName] && targetItem[tagIDKeyName] === sourceItem[tagIDKeyName]) {
|
||||
const targetTemplate = targetItem[metaTemplateKeyName]
|
||||
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
||||
if (targetTemplate && !sourceTemplate) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component)(targetTemplate)(sourceItem[contentKeyName])
|
||||
}
|
||||
// If template defined in child but content in parent
|
||||
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
|
||||
sourceItem[contentKeyName] = applyTemplate(component)(sourceTemplate)(targetItem[contentKeyName])
|
||||
delete sourceItem[metaTemplateKeyName]
|
||||
}
|
||||
shared = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!shared) {
|
||||
destination.push(targetItem)
|
||||
}
|
||||
}
|
||||
|
||||
return destination.concat(source)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove all "template" tags from meta
|
||||
|
||||
// backup the title chunk in case user wants access to it
|
||||
if (info.title) {
|
||||
info.titleChunk = info.title
|
||||
}
|
||||
|
||||
// replace title with populated template
|
||||
if (info.titleTemplate) {
|
||||
info.title = applyTemplate(component)(info.titleTemplate)(info.titleChunk || '')
|
||||
}
|
||||
|
||||
// convert base tag to an array so it can be handled the same way
|
||||
// as the other tags
|
||||
if (info.base) {
|
||||
info.base = Object.keys(info.base).length ? [info.base] : []
|
||||
}
|
||||
|
||||
const ref = info.__dangerouslyDisableSanitizers
|
||||
const refByTagID = info.__dangerouslyDisableSanitizersByTagID
|
||||
|
||||
// sanitizes potentially dangerous characters
|
||||
const escape = (info) => Object.keys(info).reduce((escaped, key) => {
|
||||
let isDisabled = ref && ref.indexOf(key) > -1
|
||||
const tagID = info[tagIDKeyName]
|
||||
if (!isDisabled && tagID) {
|
||||
isDisabled = refByTagID && refByTagID[tagID] && refByTagID[tagID].indexOf(key) > -1
|
||||
}
|
||||
const val = info[key]
|
||||
escaped[key] = val
|
||||
if (key === '__dangerouslyDisableSanitizers' || key === '__dangerouslyDisableSanitizersByTagID') {
|
||||
return escaped
|
||||
}
|
||||
if (!isDisabled) {
|
||||
if (typeof val === 'string') {
|
||||
escaped[key] = escapeHTML(val)
|
||||
} else if (isPlainObject(val)) {
|
||||
escaped[key] = escape(val)
|
||||
} else if (isArray(val)) {
|
||||
escaped[key] = val.map(escape)
|
||||
} else {
|
||||
escaped[key] = val
|
||||
}
|
||||
} else {
|
||||
escaped[key] = val
|
||||
}
|
||||
|
||||
return escaped
|
||||
}, {})
|
||||
|
||||
// merge with defaults
|
||||
info = deepmerge(defaultInfo, info)
|
||||
|
||||
// begin sanitization
|
||||
info = escape(info)
|
||||
|
||||
return info
|
||||
// backup the title chunk in case user wants access to it
|
||||
if (info.title) {
|
||||
info.titleChunk = info.title
|
||||
}
|
||||
}
|
||||
|
||||
const applyTemplate = component => template => chunk =>
|
||||
typeof template === 'function' ? template.call(component, chunk) : template.replace(/%s/g, chunk)
|
||||
// replace title with populated template
|
||||
if (info.titleTemplate && info.titleTemplate !== '%s') {
|
||||
applyTemplate({ component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || '')
|
||||
}
|
||||
|
||||
// convert base tag to an array so it can be handled the same way
|
||||
// as the other tags
|
||||
if (info.base) {
|
||||
info.base = Object.keys(info.base).length ? [info.base] : []
|
||||
}
|
||||
|
||||
const escapeOptions = {
|
||||
doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value)
|
||||
}
|
||||
|
||||
disableOptionKeys.forEach((disableKey, index) => {
|
||||
if (index === 0) {
|
||||
ensureIsArray(info, disableKey)
|
||||
} else if (index === 1) {
|
||||
for (const key in info[disableKey]) {
|
||||
ensureIsArray(info[disableKey], key)
|
||||
}
|
||||
}
|
||||
|
||||
escapeOptions[disableKey] = info[disableKey]
|
||||
})
|
||||
|
||||
// begin sanitization
|
||||
info = escape(info, options, escapeOptions)
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* checks if passed argument is an array
|
||||
* @param {any} arr - the object to check
|
||||
* @return {Boolean} - true if `arr` is an array
|
||||
*/
|
||||
export default function isArray (arr) {
|
||||
return Array.isArray
|
||||
? Array.isArray(arr)
|
||||
: Object.prototype.toString.call(arr) === '[object Array]'
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import { findIndex } from '../utils/array'
|
||||
import { applyTemplate } from './template'
|
||||
import { metaInfoAttributeKeys } from './constants'
|
||||
|
||||
export function arrayMerge({ component, tagIDKeyName, metaTemplateKeyName, contentKeyName }, target, source) {
|
||||
// we concat the arrays without merging objects contained in,
|
||||
// but we check for a `vmid` property on each object in the array
|
||||
// using an O(1) lookup associative array exploit
|
||||
const destination = []
|
||||
|
||||
target.forEach((targetItem, targetIndex) => {
|
||||
// no tagID so no need to check for duplicity
|
||||
if (!targetItem[tagIDKeyName]) {
|
||||
destination.push(targetItem)
|
||||
return
|
||||
}
|
||||
|
||||
const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName])
|
||||
const sourceItem = source[sourceIndex]
|
||||
|
||||
// source doesnt contain any duplicate vmid's, we can keep targetItem
|
||||
if (sourceIndex === -1) {
|
||||
destination.push(targetItem)
|
||||
return
|
||||
}
|
||||
|
||||
// when sourceItem explictly defines contentKeyName or innerHTML as undefined, its
|
||||
// an indication that we need to skip the default behaviour or child has preference over parent
|
||||
// which means we keep the targetItem and ignore/remove the sourceItem
|
||||
if ((sourceItem.hasOwnProperty(contentKeyName) && sourceItem[contentKeyName] === undefined) ||
|
||||
(sourceItem.hasOwnProperty('innerHTML') && sourceItem.innerHTML === undefined)) {
|
||||
destination.push(targetItem)
|
||||
// remove current index from source array so its not concatenated to destination below
|
||||
source.splice(sourceIndex, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem
|
||||
|
||||
// if source specifies null as content then ignore both the target as the source
|
||||
if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) {
|
||||
// remove current index from source array so its not concatenated to destination below
|
||||
source.splice(sourceIndex, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// now we only need to check if the target has a template to combine it with the source
|
||||
const targetTemplate = targetItem[metaTemplateKeyName]
|
||||
if (!targetTemplate) {
|
||||
return
|
||||
}
|
||||
|
||||
const sourceTemplate = sourceItem[metaTemplateKeyName]
|
||||
|
||||
if (!sourceTemplate) {
|
||||
// use parent template and child content
|
||||
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, targetTemplate)
|
||||
} else if (!sourceItem[contentKeyName]) {
|
||||
// use child template and parent content
|
||||
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, undefined, targetItem[contentKeyName])
|
||||
}
|
||||
})
|
||||
|
||||
return destination.concat(source)
|
||||
}
|
||||
|
||||
export function merge(target, source, options = {}) {
|
||||
// remove properties explicitly set to false so child components can
|
||||
// optionally _not_ overwrite the parents content
|
||||
// (for array properties this is checked in arrayMerge)
|
||||
if (source.hasOwnProperty('title') && source.title === undefined) {
|
||||
delete source.title
|
||||
}
|
||||
|
||||
metaInfoAttributeKeys.forEach((attrKey) => {
|
||||
if (!source[attrKey]) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const key in source[attrKey]) {
|
||||
if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) {
|
||||
delete source[attrKey][key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return deepmerge(target, source, {
|
||||
arrayMerge: (t, s) => arrayMerge(options, t, s)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isUndefined, isObject } from '../utils/is-type'
|
||||
|
||||
// Vue $root instance has a _vueMeta object property, otherwise its a boolean true
|
||||
export function hasMetaInfo(vm = this) {
|
||||
return vm && (vm._vueMeta === true || isObject(vm._vueMeta))
|
||||
}
|
||||
|
||||
// a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has
|
||||
export function inMetaInfoBranch(vm = this) {
|
||||
return vm && !isUndefined(vm._vueMeta)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import triggerUpdate from '../client/triggerUpdate'
|
||||
import { isUndefined, isFunction } from '../utils/is-type'
|
||||
import { ensuredPush } from '../utils/ensure'
|
||||
import { hasMetaInfo } from './meta-helpers'
|
||||
import { addNavGuards } from './nav-guards'
|
||||
|
||||
export default function createMixin(Vue, options) {
|
||||
// for which Vue lifecycle hooks should the metaInfo be refreshed
|
||||
const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']
|
||||
|
||||
// watch for client side component updates
|
||||
return {
|
||||
beforeCreate() {
|
||||
Object.defineProperty(this, '_hasMetaInfo', {
|
||||
get() {
|
||||
// Show deprecation warning once when devtools enabled
|
||||
if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) {
|
||||
console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead') // eslint-disable-line no-console
|
||||
this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true
|
||||
}
|
||||
return hasMetaInfo(this)
|
||||
}
|
||||
})
|
||||
|
||||
// Add a marker to know if it uses metaInfo
|
||||
// _vnode is used to know that it's attached to a real component
|
||||
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
|
||||
if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) {
|
||||
if (!this.$root._vueMeta) {
|
||||
this.$root._vueMeta = {}
|
||||
}
|
||||
|
||||
// to speed up updates we keep track of branches which have a component with vue-meta info defined
|
||||
// if _vueMeta = true it has info, if _vueMeta = false a child has info
|
||||
if (!this._vueMeta) {
|
||||
this._vueMeta = true
|
||||
|
||||
let p = this.$parent
|
||||
while (p && p !== this.$root) {
|
||||
if (isUndefined(p._vueMeta)) {
|
||||
p._vueMeta = false
|
||||
}
|
||||
p = p.$parent
|
||||
}
|
||||
}
|
||||
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (isFunction(this.$options[options.keyName])) {
|
||||
if (!this.$options.computed) {
|
||||
this.$options.computed = {}
|
||||
}
|
||||
this.$options.computed.$metaInfo = this.$options[options.keyName]
|
||||
|
||||
if (!this.$isServer) {
|
||||
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
||||
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
||||
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
||||
ensuredPush(this.$options, 'created', () => {
|
||||
this.$watch('$metaInfo', function () {
|
||||
triggerUpdate(this, 'watcher')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// force an initial refresh on page load and prevent other lifecycleHooks
|
||||
// to triggerUpdate until this initial refresh is finished
|
||||
// this is to make sure that when a page is opened in an inactive tab which
|
||||
// has throttled rAF/timers we still immediately set the page title
|
||||
if (isUndefined(this.$root._vueMeta.initialized)) {
|
||||
this.$root._vueMeta.initialized = this.$isServer
|
||||
|
||||
if (!this.$root._vueMeta.initialized) {
|
||||
ensuredPush(this.$options, 'mounted', () => {
|
||||
if (!this.$root._vueMeta.initialized) {
|
||||
// refresh meta in nextTick so all child components have loaded
|
||||
this.$nextTick(function () {
|
||||
this.$root.$meta().refresh()
|
||||
this.$root._vueMeta.initialized = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// add the navigation guards if requested
|
||||
if (options.refreshOnceOnNavigation) {
|
||||
addNavGuards(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do not trigger refresh on the server side
|
||||
if (!this.$isServer) {
|
||||
// add the navigation guards if they havent been added yet
|
||||
// if metaInfo is defined as a function, this does call the computed fn redundantly
|
||||
// but as Vue internally caches the results of computed props it shouldnt hurt performance
|
||||
if (!options.refreshOnceOnNavigation && (
|
||||
(this.$options[options.keyName] && this.$options[options.keyName].afterNavigation) ||
|
||||
(this.$options.computed && this.$options.computed.$metaInfo && (this.$options.computed.$metaInfo() || {}).afterNavigation)
|
||||
)) {
|
||||
addNavGuards(this)
|
||||
}
|
||||
|
||||
// no need to add this hooks on server side
|
||||
updateOnLifecycleHook.forEach((lifecycleHook) => {
|
||||
ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook))
|
||||
})
|
||||
|
||||
// re-render meta data when returning from a child component to parent
|
||||
ensuredPush(this.$options, 'destroyed', () => {
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
/* istanbul ignore next line */
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
|
||||
if (!this.$parent) {
|
||||
/* istanbul ignore next line */
|
||||
return
|
||||
}
|
||||
|
||||
triggerUpdate(this, 'destroyed')
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { isFunction } from '../utils/is-type'
|
||||
|
||||
export function addNavGuards(vm) {
|
||||
// return when nav guards already added or no router exists
|
||||
if (vm.$root._vueMeta.navGuards || !vm.$root.$router) {
|
||||
/* istanbul ignore next */
|
||||
return
|
||||
}
|
||||
|
||||
vm.$root._vueMeta.navGuards = true
|
||||
|
||||
const $router = vm.$root.$router
|
||||
const $meta = vm.$root.$meta()
|
||||
|
||||
$router.beforeEach((to, from, next) => {
|
||||
$meta.pause()
|
||||
next()
|
||||
})
|
||||
|
||||
$router.afterEach(() => {
|
||||
const { metaInfo } = $meta.resume()
|
||||
if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) {
|
||||
metaInfo.afterNavigation(metaInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isObject } from '../utils/is-type'
|
||||
import { defaultOptions } from './constants'
|
||||
|
||||
export function setOptions(options) {
|
||||
// combine options
|
||||
options = isObject(options) ? options : {}
|
||||
|
||||
for (const key in defaultOptions) {
|
||||
if (!options[key]) {
|
||||
options[key] = defaultOptions[key]
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
export function getOptions(options) {
|
||||
const optionsCopy = {}
|
||||
for (const key in options) {
|
||||
optionsCopy[key] = options[key]
|
||||
}
|
||||
return optionsCopy
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export function pause(refresh = true) {
|
||||
this.$root._vueMeta.paused = true
|
||||
|
||||
return () => resume(refresh)
|
||||
}
|
||||
|
||||
export function resume(refresh = true) {
|
||||
this.$root._vueMeta.paused = false
|
||||
|
||||
if (refresh) {
|
||||
return this.$root.$meta().refresh()
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import assign from 'object-assign'
|
||||
import $meta from './$meta'
|
||||
import batchUpdate from '../client/batchUpdate'
|
||||
|
||||
import {
|
||||
VUE_META_KEY_NAME,
|
||||
VUE_META_ATTRIBUTE,
|
||||
VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
||||
VUE_META_TAG_LIST_ID_KEY_NAME,
|
||||
VUE_META_TEMPLATE_KEY_NAME, VUE_META_CONTENT_KEY
|
||||
} from './constants'
|
||||
|
||||
// automatic install
|
||||
if (typeof window !== 'undefined' && typeof window.Vue !== 'undefined') {
|
||||
Vue.use(VueMeta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
export default function VueMeta (Vue, options = {}) {
|
||||
// set some default options
|
||||
const defaultOptions = {
|
||||
keyName: VUE_META_KEY_NAME,
|
||||
contentKeyName: VUE_META_CONTENT_KEY,
|
||||
metaTemplateKeyName: VUE_META_TEMPLATE_KEY_NAME,
|
||||
attribute: VUE_META_ATTRIBUTE,
|
||||
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
|
||||
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
|
||||
}
|
||||
// combine options
|
||||
options = assign(defaultOptions, options)
|
||||
|
||||
// bind the $meta method to this component instance
|
||||
Vue.prototype.$meta = $meta(options)
|
||||
|
||||
// store an id to keep track of DOM updates
|
||||
let batchID = null
|
||||
|
||||
// watch for client side component updates
|
||||
Vue.mixin({
|
||||
beforeCreate () {
|
||||
// Add a marker to know if it uses metaInfo
|
||||
// _vnode is used to know that it's attached to a real component
|
||||
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
|
||||
if (typeof this.$options[options.keyName] !== 'undefined') {
|
||||
this._hasMetaInfo = true
|
||||
}
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
if (typeof this.$options[options.keyName] === 'function') {
|
||||
if (typeof this.$options.computed === 'undefined') {
|
||||
this.$options.computed = {}
|
||||
}
|
||||
this.$options.computed.$metaInfo = this.$options[options.keyName]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
||||
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
||||
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
||||
if (!this.$isServer && this.$metaInfo) {
|
||||
this.$watch('$metaInfo', () => {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
})
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
if (this._hasMetaInfo) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
}
|
||||
},
|
||||
deactivated () {
|
||||
if (this._hasMetaInfo) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
}
|
||||
},
|
||||
beforeMount () {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
if (this._hasMetaInfo) {
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
// do not trigger refresh on the server side
|
||||
if (this.$isServer) return
|
||||
// re-render meta data when returning from a child component to parent
|
||||
if (this._hasMetaInfo) {
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) return
|
||||
clearInterval(interval)
|
||||
if (!this.$parent) return
|
||||
batchID = batchUpdate(batchID, () => this.$meta().refresh())
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isUndefined, isFunction } from '../utils/is-type'
|
||||
|
||||
export function applyTemplate({ component, metaTemplateKeyName, contentKeyName }, headObject, template, chunk) {
|
||||
if (isUndefined(template)) {
|
||||
template = headObject[metaTemplateKeyName]
|
||||
delete headObject[metaTemplateKeyName]
|
||||
}
|
||||
|
||||
// return early if no template defined
|
||||
if (!template) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isUndefined(chunk)) {
|
||||
chunk = headObject[contentKeyName]
|
||||
}
|
||||
|
||||
headObject[contentKeyName] = isFunction(template)
|
||||
? template.call(component, chunk)
|
||||
: template.replace(/%s/g, chunk)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function uniqBy (inputArray, predicate) {
|
||||
return inputArray
|
||||
.filter((x, i, arr) => i === arr.length - 1
|
||||
? true
|
||||
: predicate(x) !== predicate(arr[i + 1])
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* To reduce build size, this file provides simple polyfills without
|
||||
* overly excessive type checking and without modifying
|
||||
* the global Array.prototype
|
||||
* The polyfills are automatically removed in the commonjs build
|
||||
* Also, only files in client/ & shared/ should use these functions
|
||||
* files in server/ still use normal js function
|
||||
*/
|
||||
|
||||
// this const is replaced by rollup to true for umd builds
|
||||
// which means the polyfills are removed for other build formats
|
||||
const polyfill = process.env.NODE_ENV === 'test'
|
||||
|
||||
export function findIndex(array, predicate) {
|
||||
if (polyfill && !Array.prototype.findIndex) {
|
||||
// idx needs to be a Number, for..in returns string
|
||||
for (let idx = 0; idx < array.length; idx++) {
|
||||
if (predicate.call(arguments[2], array[idx], idx, array)) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return array.findIndex(predicate, arguments[2])
|
||||
}
|
||||
|
||||
export function toArray(arg) {
|
||||
if (polyfill && !Array.from) {
|
||||
return Array.prototype.slice.call(arg)
|
||||
}
|
||||
return Array.from(arg)
|
||||
}
|
||||
|
||||
export function includes(array, value) {
|
||||
if (polyfill && !Array.prototype.includes) {
|
||||
for (const idx in array) {
|
||||
if (array[idx] === value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return array.includes(value)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isArray, isObject } from './is-type'
|
||||
|
||||
export function ensureIsArray(arg, key) {
|
||||
if (!key || !isObject(arg)) {
|
||||
return isArray(arg) ? arg : []
|
||||
}
|
||||
|
||||
if (!isArray(arg[key])) {
|
||||
arg[key] = []
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
export function ensuredPush(object, key, el) {
|
||||
ensureIsArray(object, key)
|
||||
|
||||
object[key].push(el)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* checks if passed argument is an array
|
||||
* @param {any} arg - the object to check
|
||||
* @return {Boolean} - true if `arg` is an array
|
||||
*/
|
||||
export function isArray(arg) {
|
||||
return Array.isArray(arg)
|
||||
}
|
||||
|
||||
export function isUndefined(arg) {
|
||||
return typeof arg === 'undefined'
|
||||
}
|
||||
|
||||
export function isObject(arg) {
|
||||
return typeof arg === 'object'
|
||||
}
|
||||
|
||||
export function isFunction(arg) {
|
||||
return typeof arg === 'function'
|
||||
}
|
||||
|
||||
export function isString(arg) {
|
||||
return typeof arg === 'string'
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isUndefined } from './is-type'
|
||||
|
||||
export function hasGlobalWindowFn() {
|
||||
try {
|
||||
return !isUndefined(window)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const hasGlobalWindow = hasGlobalWindowFn()
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<hello-world v-if="childVisible"></hello-world>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from './hello-world.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HelloWorld
|
||||
},
|
||||
props: {
|
||||
changed: {
|
||||
type: Function
|
||||
}
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
changed: this._changed
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
childVisible: false,
|
||||
_changed: () => {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this._changed = this.changed.bind(this)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<hello-world v-if="childVisible"></hello-world>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from './hello-world.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HelloWorld
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
childVisible: true,
|
||||
title: 'Goodbye World'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div>Test</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: 'Hello World',
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
},
|
||||
meta: [
|
||||
{ charset: 'utf-8' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<keep-alive>
|
||||
<hello-world v-if="childVisible"></hello-world>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from './hello-world.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HelloWorld
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
childVisible: true,
|
||||
title: 'Alive World'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import env from 'node-env-file'
|
||||
import { createBrowser } from 'tib'
|
||||
|
||||
const browserString = process.env.BROWSER_STRING || 'puppeteer/core'
|
||||
|
||||
describe(browserString, () => {
|
||||
let browser
|
||||
let page
|
||||
const folder = path.resolve(__dirname, '..', 'fixtures/basic/.vue-meta/')
|
||||
|
||||
beforeAll(async () => {
|
||||
if (browserString.includes('browserstack') && browserString.includes('local')) {
|
||||
const envFile = path.resolve(__dirname, '..', '..', '.env-browserstack')
|
||||
if (fs.existsSync(envFile)) {
|
||||
env(envFile)
|
||||
}
|
||||
}
|
||||
|
||||
browser = await createBrowser(browserString, {
|
||||
BrowserStackLocal: { folder },
|
||||
extendPage(page) {
|
||||
return {
|
||||
async navigate(path) {
|
||||
// IMPORTANT: use (arrow) function with block'ed body
|
||||
// see: https://github.com/tunnckoCoreLabs/parse-function/issues/179
|
||||
await page.runAsyncScript((path) => {
|
||||
return new Promise((resolve) => {
|
||||
const oldTitle = document.title
|
||||
|
||||
// local firefox has sometimes not updated the title
|
||||
// even when the DOM is supposed to be fully updated
|
||||
const waitTitleChanged = function () {
|
||||
setTimeout(function () {
|
||||
if (oldTitle !== document.title) {
|
||||
resolve()
|
||||
} else {
|
||||
waitTitleChanged()
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
window.$vueMeta.$once('routeChanged', waitTitleChanged)
|
||||
window.$vueMeta.$router.push(path)
|
||||
})
|
||||
}, path)
|
||||
},
|
||||
routeData() {
|
||||
return page.runScript(() => ({
|
||||
path: window.$vueMeta.$route.path,
|
||||
query: window.$vueMeta.$route.query
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (browser) {
|
||||
await browser.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('open page', async () => {
|
||||
const webPath = '/index.html'
|
||||
|
||||
let url
|
||||
if (browser.getLocalFolderUrl) {
|
||||
url = browser.getLocalFolderUrl(webPath)
|
||||
} else {
|
||||
url = `file://${path.join(folder, webPath)}`
|
||||
}
|
||||
|
||||
page = await browser.page(url)
|
||||
|
||||
expect(await page.getAttribute('html', 'data-vue-meta-server-rendered')).toBe(null)
|
||||
expect(await page.getAttribute('html', 'lang')).toBe('en')
|
||||
expect(await page.getAttribute('html', 'amp')).toBe('')
|
||||
expect(await page.getAttribute('html', 'allowfullscreen')).toBe(null)
|
||||
expect(await page.getAttribute('head', 'test')).toBe('true')
|
||||
expect(await page.getText('h1')).toBe('Basic')
|
||||
expect(await page.getText('title')).toBe('Home | Vue Meta Test')
|
||||
expect(await page.getElementCount('meta')).toBe(2)
|
||||
|
||||
let sanitizeCheck = await page.getTexts('script')
|
||||
sanitizeCheck.push(...(await page.getTexts('noscript')))
|
||||
sanitizeCheck = sanitizeCheck.filter(v => !!v)
|
||||
|
||||
expect(sanitizeCheck.length).toBe(3)
|
||||
expect(() => JSON.parse(sanitizeCheck[0])).not.toThrow()
|
||||
// TODO: check why this doesnt Throw when Home is dynamic loaded
|
||||
// (but that causes hydration error)
|
||||
expect(() => JSON.parse(sanitizeCheck[1])).toThrow()
|
||||
expect(() => JSON.parse(sanitizeCheck[2])).not.toThrow()
|
||||
})
|
||||
|
||||
test('/about', async () => {
|
||||
try {
|
||||
await page.navigate('/about', false)
|
||||
} catch (e) {
|
||||
if (e.constructor.name !== 'ScriptTimeoutError') {
|
||||
throw e
|
||||
} else {
|
||||
console.warn(e) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
expect(await page.getText('title')).toBe('About')
|
||||
expect(await page.getElementCount('meta')).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { buildFixture } from '../utils/build'
|
||||
|
||||
describe('basic browser with ssr page', () => {
|
||||
let html
|
||||
|
||||
beforeAll(async () => {
|
||||
const fixture = await buildFixture('basic')
|
||||
html = fixture.html
|
||||
})
|
||||
|
||||
test('validate ssr', () => {
|
||||
const htmlTag = html.match(/<html([^>]+)>/)[0]
|
||||
expect(htmlTag).toContain('data-vue-meta-server-rendered')
|
||||
expect(htmlTag).toContain(' lang="en" ')
|
||||
expect(htmlTag).toContain(' amp ')
|
||||
expect(htmlTag).not.toContain('allowfullscreen')
|
||||
expect(html.match(/<title[^>]*>(.*?)<\/title>/)[1]).toBe('Home | Vue Meta Test')
|
||||
expect(html.match(/<meta/g).length).toBe(2)
|
||||
expect(html.match(/<meta/g).length).toBe(2)
|
||||
|
||||
const re = /<(no)?script[^>]+type="application\/ld\+json"[^>]*>(.*?)</g
|
||||
const sanitizeCheck = []
|
||||
let match
|
||||
while ((match = re.exec(html))) {
|
||||
sanitizeCheck.push(match[2])
|
||||
}
|
||||
|
||||
expect(sanitizeCheck.length).toBe(3)
|
||||
expect(() => JSON.parse(sanitizeCheck[0])).not.toThrow()
|
||||
expect(() => JSON.parse(sanitizeCheck[1])).toThrow()
|
||||
expect(() => JSON.parse(sanitizeCheck[2])).not.toThrow()
|
||||
})
|
||||
})
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html data-vue-meta-server-rendered {{ htmlAttrs.text() }}>
|
||||
<head {{ headAttrs.text() }}>
|
||||
{{ meta.text() }}
|
||||
{{ title.text() }}
|
||||
{{ link.text() }}
|
||||
{{ style.text() }}
|
||||
{{ webpackAssets }}
|
||||
{{ script.text() }}
|
||||
{{ noscript.text() }}
|
||||
</head>
|
||||
<body {{ bodyAttrs.text() }}>
|
||||
{{ app }}
|
||||
{{ script.text({ body: true }) }}
|
||||
{{ noscript.text({ body: true }) }}
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<h1>Basic</h1>
|
||||
<router-view></router-view>
|
||||
<p>Inspect Element to see the meta info</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
metaInfo() {
|
||||
return {
|
||||
meta: [
|
||||
{ vmid: 'charset', charset: 'utf-8' }
|
||||
],
|
||||
afterNavigation: () => {
|
||||
this.$emit('routeChanged')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.$vueMeta = this
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import VueMeta from '../../../src/browser'
|
||||
import App from './App.vue'
|
||||
import createRouter from './router'
|
||||
|
||||
Vue.use(VueMeta)
|
||||
|
||||
App.router = createRouter()
|
||||
|
||||
new Vue(App).$mount('#app')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user