├── .gitignore ├── README.md ├── book.json ├── docs ├── README.md ├── SUMMARY.md ├── backends.md ├── components │ ├── autolang.md │ ├── breadcrumb.md │ ├── canonical.md │ ├── description.md │ ├── image.md │ ├── index.md │ ├── lang.md │ ├── more.md │ ├── organization.md │ ├── title.md │ └── type.md ├── customPolicies.md ├── install.md ├── policies.md ├── predefinedPolicies.md └── urlChanges.md ├── examples ├── autoLang │ ├── demo.details │ ├── demo.html │ └── demo.js ├── breadcrumb │ ├── demo.details │ ├── demo.html │ └── demo.js ├── joinedTitle │ ├── demo.details │ ├── demo.html │ └── demo.js ├── lang │ ├── demo.details │ ├── demo.html │ └── demo.js └── title │ ├── demo.details │ ├── demo.html │ └── demo.js ├── index.js ├── package.json ├── src ├── components │ ├── autoLang.vue │ ├── breadcrumbs.vue │ ├── canonical.vue │ ├── description.vue │ ├── head.vue │ ├── image.vue │ ├── lang.vue │ ├── organization.vue │ ├── title.vue │ └── type.vue ├── google │ ├── breadcrumbsDisplayer.vue │ ├── canonicalDisplayer.vue │ ├── descriptionDisplayer.vue │ ├── displayer.vue │ ├── langDisplayer.vue │ ├── organizationDisplayer.vue │ ├── titleDisplayer.vue │ └── websiteDisplayer.vue ├── main.js ├── middlewares │ ├── autoLangDisplayer.vue │ └── displayer.vue ├── mixins │ ├── displayer.vue │ ├── ldDisplayer.js │ ├── optionAccess.js │ ├── storeAccess.js │ └── writer.js ├── openGraph │ ├── descriptionDisplayer.vue │ ├── displayer.vue │ ├── imageDisplayer.vue │ ├── langDisplayer.vue │ ├── organizationDisplayer.vue │ ├── titleDisplayer.vue │ ├── typeDisplayer.vue │ └── urlDisplayer.vue └── utils │ ├── store.js │ ├── urlChanged.js │ └── vueStore.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | index.js.map 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | node_modules 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | _book 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `vue-seo` 2 | 3 | ## [Full Documentation](http://guillaumeleclerc.github.io/vue-seo/) 4 | 5 | ## Aim 6 | 7 | The aim of this Vue plugin is to help you indexing your Vue powered website using all the greatness of Vue. We also want to give shorthands for common tasks that are usually verbose and reduce the readability of your code. 8 | 9 | 10 | # Showcase 11 | 12 | With this plugin you are able to use this kind of syntax in any component (does not need to be in the head): 13 | 14 | ### Complex title management 15 | 16 | ```html 17 | 21 | ``` 22 | 23 | 24 | ### Describe your company 25 | ```html 26 | 31 | ``` 32 | 33 | ### Merge policies 34 | ``` 35 | 36 | 37 | ``` 38 | 39 | with a merge policy for title `VueSEO.policies.join(' - ')` and notifications equal to 3. The title will be: 40 | 41 | `My website - 3 notif.` 42 | 43 | __note__: you can define the two `seo-title` components at different places wherever you want. 44 | 45 | If you want to know more about all the other features just browser the documentation. 46 | 47 | ## Sponsor 48 | 49 | The development of this plugin is made possible by [Papayapods](http://papayapods.com) 50 | 51 | 52 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "2.5.2", 3 | "structure": { 4 | "summary": "docs/SUMMARY.md" 5 | }, 6 | "plugins": ["edit-link", "prism", "-highlight", "github", "include-codeblock"], 7 | "pluginsConfig": { 8 | "edit-link": { 9 | "base": "https://github.com/GuillaumeLeclerc/vue-seo/tree/master", 10 | "label": "Edit This Page" 11 | }, 12 | "github": { 13 | "url": "https://github.com/GuillaumeLeclerc/vue-seo/" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # `vue-seo` 2 | 3 | ## Aim 4 | 5 | The aim of this Vue plugin is to help you indexing your Vue powered website using all the greatness of Vue. We also want to give shorthands for common tasks that are usually verbose and reduce the readability of your code. 6 | 7 | 8 | # Showcase 9 | 10 | With this plugin you are able to use this kind of syntax in any component (does not need to be in the head): 11 | 12 | ### Complex title management 13 | 14 | ```html 15 | 19 | ``` 20 | 21 | 22 | ### Describe your company 23 | ```html 24 | 29 | ``` 30 | 31 | If you want to know more about all the other features just browser the documentation. 32 | 33 | ## Sponsor 34 | 35 | The development of this plugin is made possible by [Papayapods](http://papayapods.com) 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of content 2 | 3 | * [Getting Started](docs/install.md) 4 | * [All Components](docs/components/index.md) 5 | * [Title](docs/components/title.md) 6 | * [Description](docs/components/description.md) 7 | * [Organization](docs/components/organization.md) 8 | * [Breadcrumb](docs/components/breadcrumb.md) 9 | * [Canonical](docs/components/canonical.md) 10 | * [Lang](docs/components/lang.md) 11 | * [AutoLang](docs/components/autolang.md) 12 | * [Image](docs/components/image.md) 13 | * [Type](docs/components/type.md) 14 | * [I want mooore !](docs/components/more.md) 15 | * [Backends](docs/backends.md) 16 | * [Url Changes](docs/urlChanges.md) 17 | * [Policies](docs/policies.md) 18 | * [Default Policies](docs/predefinedPolicies.md) 19 | * [Create your own policy](docs/customPolicies.md) 20 | -------------------------------------------------------------------------------- /docs/backends.md: -------------------------------------------------------------------------------- 1 | # Backends 2 | 3 | The `seo-*` components enable features on different backends. The following backends are currently supported: 4 | 5 | - __Google__: The components help google understading your data and display it in an enhanced way to your users 6 | - __OpenGraph__: The components helps sharing plateform to understand your data and display them in a good way the following website support this: 7 | - Facebook sharing 8 | - Linkedin sharing 9 | - Google + 10 | - mixi 11 | - Know others? make a pull request :) 12 | 13 | 14 | __IMPORTANT__: To follow the OpenGraph specification you need 15 | - The url (infered automatically by `vue-seo`) 16 | - The type: You absolutely need to have a `` component 17 | - At least one image: set through `` 18 | - A title: set through `` 19 | 20 | 21 | __IMPORTANT__: As of today, Facebook bots does noes render javascript (does not run Vue code). If you want it to be effective you will need to use a prerenderer such as: 22 | - [Prerender-node](https://github.com/prerender/prerender-node) 23 | - [Prerender.io](https://prerender.io) 24 | 25 | Or if you feel risky you can try [`vue-server`](https://github.com/ngsru/vue-server) 26 | 27 | ## Disabling Backends 28 | 29 | If you want to disable a backend you can add the following in the options of VueSEO: 30 | 31 | - __Google__: `google: false` 32 | - __OpenGraph__: `openGraph: false` 33 | 34 | ## Adding Backend 35 | 36 | It's not possible at the moment to add custom backends. If you want this feature (or another) you can make an Issue on the repository. 37 | -------------------------------------------------------------------------------- /docs/components/autolang.md: -------------------------------------------------------------------------------- 1 | # AutoLang (`seo-auto-lang`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 11 | ``` 12 | 13 | ## Props 14 | 15 | - __langs__: The langs the website is available in 16 | - __mode__: How you encode the lang in the url, supported formats: 17 | - `domain`: The lang is an is an ISO-639-1 lang code put as a subdomain. Eg: 'fr.papayapods.com'. 18 | - __default__: This is the default language. It will be ommited in the urls generateds 19 | 20 | ## Impact 21 | 22 | This component will generate automatically `` components based on the current url in all the langs you defined. And automatically updates when the url change. 23 | 24 | 25 | ## Example 26 | 27 | [Try this fiddle](http://jsfiddle.net/gh/get/library/pure/GuillaumeLeclerc/vue-seo/tree/master/examples/autoLang) 28 | 29 | [include](../../examples/autoLang/demo.html) 30 | [include](../../examples/autoLang/demo.js) 31 | -------------------------------------------------------------------------------- /docs/components/breadcrumb.md: -------------------------------------------------------------------------------- 1 | # Breadcrumb (`seo-breadcrumb`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 7 | 11 | ``` 12 | 13 | ## Props 14 | 15 | - __name__: The name to display in the google result for this part of the breadcrumb 16 | - __url__: The url of this part of the breadcrumb 17 | - position: (optional) The position of this fragment in the breadcrumb hierarchy 18 | 19 | ## Impact 20 | 21 | - __Google__: Add readable and clickable breadcrumbs to your search results 22 | 23 | ![Breadcrumbs in action](https://developers.google.com/structured-data/images/breadcrumbs.png) 24 | 25 | ## Tip 26 | 27 | If you are makin a single page application then you can define a `seo-breadcrumb` in every component. Every time the user (=search bot) switch to anoter page anoter component will change and the breadcrumb will be updated ! 28 | 29 | ## Example 30 | 31 | To have the same result as in the previous image you could do this 32 | 33 | [Try this fiddle](http://jsfiddle.net/gh/get/library/pure/GuillaumeLeclerc/vue-seo/tree/master/examples/breadcrumb) 34 | 35 | [include](../../examples/breadcrumb/demo.html) 36 | [include](../../examples/breadcrumb/demo.js) 37 | 38 | -------------------------------------------------------------------------------- /docs/components/canonical.md: -------------------------------------------------------------------------------- 1 | # Description (`seo-canonical`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 9 | ``` 10 | 11 | ## Props 12 | 13 | - __value__ : The canonical link of your page 14 | 15 | ## Impact 16 | 17 | - __Google__: Sets the canonical link of the page 18 | -------------------------------------------------------------------------------- /docs/components/description.md: -------------------------------------------------------------------------------- 1 | # Description (`seo-description`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 9 | ``` 10 | 11 | ## Props 12 | 13 | - __value__ : The description of your page 14 | 15 | ## Impact 16 | 17 | - __Google__: Sets the description of the page (Whats displayed under the title in search resutls) 18 | - __OpenGraph/Facebook search__: Sets the description of your item in the open graph 19 | -------------------------------------------------------------------------------- /docs/components/image.md: -------------------------------------------------------------------------------- 1 | # Image (`seo-image`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 11 | ``` 12 | 13 | ## Props 14 | 15 | - __url__: The url of the image 16 | - __width__: The width of the image 17 | - __height__: The height of the image 18 | 19 | ## Impact 20 | 21 | - __Google__: 22 | - Not (Yet), We are investingating if the Schema.org ImageItem is usefull 23 | - __OpenGraph / Facebook share__: 24 | - Adds one more image describing your open graph item, ie. the picture displayed when you share this item 25 | 26 | -------------------------------------------------------------------------------- /docs/components/index.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | For the moment these components are available: 4 | 5 | * [Title](title.md) 6 | * [Meta](meta.md) 7 | * [Organization](organization.md) 8 | * [Breadcrumb](breadcrumb.md) 9 | * [HrefLang](hreflang.md) 10 | 11 | But if you think something is missing read [THIS PAGE](more.md) 12 | -------------------------------------------------------------------------------- /docs/components/lang.md: -------------------------------------------------------------------------------- 1 | # Lang (`seo-lang`) 2 | 3 | This components is here to warn search engines your page is available in other languages 4 | 5 | ## Syntax 6 | 7 | ```html 8 | 9 | ``` 10 | 11 | ## Props 12 | 13 | - __code__ : The code for this language 14 | - __url__ : The url of the equivalent page 15 | - __current__: Tells if this is the current displayed language 16 | 17 | ## Impact 18 | 19 | - __Google__: 20 | - Add Hreflang tags, You can read [this](https://en.wikipedia.org/wiki/Hreflang) 21 | - __OpenGraph / Facebook Search__: 22 | - Add locale to your open graph 23 | - Add alternative locales 24 | 25 | ## Example 26 | 27 | [Try this fiddle](http://jsfiddle.net/gh/get/library/pure/GuillaumeLeclerc/vue-seo/tree/master/examples/lang) 28 | 29 | [include](../../examples/lang/demo.html) 30 | [include](../../examples/lang/demo.js) 31 | 32 | -------------------------------------------------------------------------------- /docs/components/more.md: -------------------------------------------------------------------------------- 1 | # More components 2 | 3 | If you think something is missing and it satisfies the following criteria: 4 | 5 | - Related to search engine optimization 6 | - Is commonly used 7 | - Has (or we suspect it) to have any impact on search engines 8 | 9 | Then create an [issue](https://github.com/GuillaumeLeclerc/vue-seo/issues) and we will try to implement it ! 10 | 11 | __important__ : We won't accept every format available on [Schema.org](http://schema.org). It must be supported by at least one search engine. 12 | -------------------------------------------------------------------------------- /docs/components/organization.md: -------------------------------------------------------------------------------- 1 | # Organization (`seo-organization`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 14 | ``` 15 | 16 | ## Props 17 | 18 | - __name__: The name of the organization 19 | - __url__: The url of the website for this organization 20 | - __logo__: The logo of this company 21 | - __contacts__: Array of contacts for this organization, see [THIS](https://developers.google.com/structured-data/customize/contact-points#company_phone_numbers#adding_structured_markup_to_your_site) 22 | - __socialAccounts__: And array of social account page (facebook, youtube, linkedin...) 23 | - __alternateName__: The alternate name of your organization/Website 24 | - __enableWebsite__: [Bool] (advanded) Does this tag describe your website 25 | - __enableOrganization__: [Bool] (advanded) Does this tag describe your organization 26 | 27 | ## Impact 28 | 29 | - __Google__: 30 | - Display a box describing your company on the right pane 31 | - Show your logo on the right pane 32 | - Add social links on the right pane 33 | - Set friendly name for your search results 34 | - Add contact phone on the right pane 35 | - __OpenGraph / Facebook share__: 36 | - Sets the title of your website 37 | - __important__: For the moment if you want to set the image of your facebook search image you have to use `` 38 | 39 | ![Examples of impact](https://developers.google.com/structured-data/images/customize-kg.png) 40 | -------------------------------------------------------------------------------- /docs/components/title.md: -------------------------------------------------------------------------------- 1 | # Title (`seo-title`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 9 | ``` 10 | 11 | ## Props 12 | 13 | - __value__ : The title of the page 14 | 15 | ## Impact 16 | 17 | - __Google__: Sets the title of the search result 18 | - __Browser__: Sets the title of the window/tab 19 | - __OpenGraph/Facebook search__: Sets the Title of your shared item 20 | 21 | ## Example 1 (Simple) 22 | 23 | [Try this fiddle](http://jsfiddle.net/gh/get/library/pure/GuillaumeLeclerc/vue-seo/tree/master/examples/title) 24 | 25 | [include](../../examples/title/demo.html) 26 | [include](../../examples/title/demo.js) 27 | 28 | ## Example 2 (Complex) 29 | 30 | Does not work for now ! 31 | This example make uses of Policies, if you don't know about it yet, read [THIS](../policies.md) 32 | 33 | [Try this fiddle](http://jsfiddle.net/gh/get/library/pure/GuillaumeLeclerc/vue-seo/tree/master/examples/joinedTitle) 34 | 35 | [include](../../examples/joinedTitle/demo.html) 36 | [include](../../examples/joinedTitle/demo.js) 37 | -------------------------------------------------------------------------------- /docs/components/type.md: -------------------------------------------------------------------------------- 1 | # Type (`seo-type`) 2 | 3 | ## Syntax 4 | 5 | ```html 6 | 9 | ``` 10 | 11 | ## Props 12 | 13 | - __value__ : The type of content (The list of available types are available [Here](http://ogp.me/#types) 14 | 15 | 16 | ## Impact 17 | 18 | - __OpenGraph/Facebook search__: Sets the type of object 19 | -------------------------------------------------------------------------------- /docs/customPolicies.md: -------------------------------------------------------------------------------- 1 | # Custom policy 2 | 3 | In order to write your own policy you need to define a function taking one argument: an array of all definitions for a given attribute and return a string 4 | 5 | ## Example 6 | 7 | ```javascript 8 | 9 | const myStupidPolicy = (possible) => { 10 | return "constant value"; 11 | } 12 | 13 | Vue.use(VueSEO, { 14 | policies: { 15 | 'title': myStupidPolicy 16 | } 17 | } 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | #Getting started 2 | 3 | ## Install 4 | 5 | ```bash 6 | npm install --save vue-seo 7 | ``` 8 | 9 | Or you can the the main javascript file and host it wherever you want (preferably on a CDN). 10 | 11 | __There are no dependency__ (except `vue>=1.0.21` of course) 12 | 13 | ## Load 14 | 15 | If you are using a bundler: 16 | 17 | ```javascript 18 | 19 | import VueSEO from 'vue-seo' 20 | ``` 21 | 22 | If you are loading script the "old" way: 23 | 24 | ```html 25 | 26 | ``` 27 | 28 | ## Start 29 | 30 | ```javascript 31 | Vue.use(VueSEO, /* options here */); 32 | ``` 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/policies.md: -------------------------------------------------------------------------------- 1 | # Policies 2 | 3 | ## Motivation 4 | 5 | What it you define something twice on your page (you could define it in the main component and the other one in a component displayed only if a certain route is matched). For example: 6 | 7 | ```html 8 | 9 | 10 | ``` 11 | 12 | What should be the title (ie. displayed on top of the user browser) ? 13 | 14 | Policies are the way to determine the behavior in this kind of situation. 15 | 16 | You can define a property for almost everything defined with `vue-seo` in the option object: 17 | 18 | ```javascript 19 | Vue.use(VueSEO, { 20 | policies: { 21 | 'title': the_policy 22 | } 23 | }); 24 | ``` 25 | 26 | __Important__: The default policiy if not specified is `VueSEO.policies.last` 27 | 28 | You can either use: 29 | - One of the [predefined policies](predefinedPolicies.md) 30 | - [Make your own policy](customPolicies.md) 31 | -------------------------------------------------------------------------------- /docs/predefinedPolicies.md: -------------------------------------------------------------------------------- 1 | #Predefined Policies 2 | 3 | The 5 predefined policies are: 4 | 5 | - `VueSEO.policies.last`: With this policy only the last defined value will be considered 6 | - `VueSEO.policies.first`: Only the first defined value will be considered 7 | - `VueSEO.policies.join(glue, reversed=false)`: All values will be considered and joined with `glue` (potenetially reversed) 8 | - `VueSEO.policies.repeat`: The displayer will be repeated for every component you make (eg. there will be 4 title tag if you have 4 seo-title) 9 | - `VueSEO.policies.identity`: The policy does not do anything. That means you need a special backend. It will have to accept an array instead of an object (you should not need it, it should be for internals only) 10 | 11 | ## Example 12 | 13 | [include](../examples/joinedTitle/demo.js) 14 | -------------------------------------------------------------------------------- /docs/urlChanges.md: -------------------------------------------------------------------------------- 1 | # Url changes 2 | 3 | Some components such as `seo-auto-lang`, needs to know when the url change to update the content. If you use hashes there will no problem. But if you are using html5 history features then there is no event to listen that will tell the component when the url ha changed. 4 | 5 | Since it's considered bad practise to just poll the content of the url bar regulary. We are giving you a way to warn `vue-seo` that the url has changed. 6 | 7 | `VueSEO` is exposing a function you need to call when the url change 8 | 9 | - __VueSEO.urlChangedNotify__ 10 | 11 | ## Example (with vue-router) 12 | 13 | ```javascript 14 | 15 | import Vue from 'vue' 16 | import VueSEO from 'vue-seo' 17 | import Router form 'vue-router' 18 | 19 | Vue.use(Router) 20 | Vue.use(VueSEO) 21 | 22 | const router = new Router({ 23 | history: true 24 | }) 25 | 26 | router.afterEach(function() { 27 | VueSEO.urlChangedNotify() 28 | }) 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/autoLang/demo.details: -------------------------------------------------------------------------------- 1 | --- 2 | name: Advanced Vue-seo title demo 3 | description: Shows how to use the title component 4 | authors: 5 | - Guillaume Leclerc 6 | normalize_css: no 7 | wrap: l 8 | ... 9 | -------------------------------------------------------------------------------- /examples/autoLang/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/autoLang/demo.js: -------------------------------------------------------------------------------- 1 | Vue.use(VueSEO, { }); 2 | 3 | new Vue({ 4 | el: 'body', 5 | }) 6 | 7 | -------------------------------------------------------------------------------- /examples/breadcrumb/demo.details: -------------------------------------------------------------------------------- 1 | --- 2 | name: Vue-seo breadcrumbs demo 3 | description: Shows how to use the breadcrumb component 4 | authors: 5 | - Guillaume Leclerc 6 | resources: 7 | - https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.21/vue.js 8 | - https://rawgit.com/GuillaumeLeclerc/vue-seo/master/index.js 9 | normalize_css: no 10 | ... 11 | -------------------------------------------------------------------------------- /examples/breadcrumb/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/breadcrumb/demo.js: -------------------------------------------------------------------------------- 1 | Vue.use(VueSEO); 2 | 3 | new Vue({ 4 | el: 'body', 5 | }) 6 | 7 | -------------------------------------------------------------------------------- /examples/joinedTitle/demo.details: -------------------------------------------------------------------------------- 1 | --- 2 | name: Advanced Vue-seo title demo 3 | description: Shows how to use the title component 4 | authors: 5 | - Guillaume Leclerc 6 | normalize_css: no 7 | wrap: l 8 | ... 9 | -------------------------------------------------------------------------------- /examples/joinedTitle/demo.html: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 | 16 |
17 | 18 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/joinedTitle/demo.js: -------------------------------------------------------------------------------- 1 | // We can set the policies for a given key in vue-seo 2 | // 3 | // The available policies are available in the docs 4 | 5 | Vue.use(VueSEO, { 6 | policies: { 7 | 'title': VueSEO.policies.join(' - ', true) 8 | } 9 | }); 10 | 11 | new Vue({ 12 | el: 'body', 13 | data () { 14 | return { 15 | inner: false, 16 | inner2: false 17 | } 18 | } 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /examples/lang/demo.details: -------------------------------------------------------------------------------- 1 | --- 2 | name: Vue-seo lang demo 3 | description: Shows how to use the lang component 4 | authors: 5 | - Guillaume Leclerc 6 | normalize_css: no 7 | wrap: l 8 | ... 9 | -------------------------------------------------------------------------------- /examples/lang/demo.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/lang/demo.js: -------------------------------------------------------------------------------- 1 | Vue.use(VueSEO); 2 | 3 | new Vue({ 4 | el: 'body', 5 | data () { 6 | return { 7 | langs: false 8 | } 9 | } 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /examples/title/demo.details: -------------------------------------------------------------------------------- 1 | --- 2 | name: Vue-seo title demo 3 | description: Shows how to use the title component 4 | authors: 5 | - Guillaume Leclerc 6 | normalize_css: no 7 | wrap: l 8 | ... 9 | -------------------------------------------------------------------------------- /examples/title/demo.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/title/demo.js: -------------------------------------------------------------------------------- 1 | Vue.use(VueSEO); 2 | 3 | new Vue({ 4 | el: 'body', 5 | data () { 6 | return { 7 | specialTitle: "This is less boring", 8 | override: false 9 | } 10 | } 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-seo", 3 | "version": "0.1.5", 4 | "description": "This is a set a vue components to do seo (main target = google)", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server --inline --hot --quiet --port 9999", 8 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", 9 | "docs:prepare": "gitbook install", 10 | "docs:watch": "npm run docs:prepare && gitbook serve", 11 | "docs:build": "npm run docs:prepare && rm -rf _book && gitbook build", 12 | "docs:publish": "npm run docs:build && cd _book && git init && git commit --allow-empty -m 'Update docs' && git checkout -b gh-pages && git add . && git commit -am 'Update docs' && git push git@github.com:GuillaumeLeclerc/vue-seo gh-pages --force" 13 | }, 14 | "dependencies": {}, 15 | "peerDependencies": { 16 | "lodash": "^3.10.1" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.1.2", 20 | "babel-loader": "^6.1.0", 21 | "babel-plugin-lodash": "^2.2.2", 22 | "babel-plugin-transform-runtime": "^6.1.2", 23 | "babel-preset-es2015": "^6.1.2", 24 | "babel-preset-stage-0": "^6.1.2", 25 | "babel-runtime": "^5.8.0", 26 | "cross-env": "^1.0.5", 27 | "css-loader": "^0.23.0", 28 | "file-loader": "^0.8.4", 29 | "iso-639-1": "^1.1.0", 30 | "jade": "^1.11.0", 31 | "less": "^2.5.3", 32 | "less-loader": "^2.2.2", 33 | "style-loader": "^0.13.0", 34 | "stylus-loader": "^1.4.0", 35 | "template-html-loader": "0.0.3", 36 | "vue-hot-reload-api": "^1.2.0", 37 | "vue-html-loader": "^1.0.0", 38 | "vue-loader": "^7.2.0", 39 | "webpack": "^1.12.2", 40 | "webpack-dev-server": "^1.12.0" 41 | }, 42 | "author": "Guillaume Leclerc", 43 | "license": "MIT", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/GuillaumeLeclerc/vue-seo.git" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/autoLang.vue: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/components/breadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /src/components/canonical.vue: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /src/components/description.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /src/components/head.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 73 | -------------------------------------------------------------------------------- /src/components/image.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /src/components/lang.vue: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/components/organization.vue: -------------------------------------------------------------------------------- 1 | 4 | 84 | -------------------------------------------------------------------------------- /src/components/title.vue: -------------------------------------------------------------------------------- 1 | 4 | 26 | -------------------------------------------------------------------------------- /src/components/type.vue: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /src/google/breadcrumbsDisplayer.vue: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /src/google/canonicalDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /src/google/descriptionDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/google/displayer.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/google/langDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/google/organizationDisplayer.vue: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /src/google/titleDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/google/websiteDisplayer.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Head from './components/head.vue' 2 | import Title from './components/title.vue' 3 | import Organization from './components/organization.vue' 4 | import Breadcrumb from './components/breadcrumbs.vue' 5 | import Canonical from './components/canonical.vue' 6 | import VueStore from './utils/vueStore.js' 7 | import Type from './components/type.vue' 8 | import Lang from './components/lang.vue' 9 | import Description from './components/description.vue' 10 | import SeoImage from './components/image.vue' 11 | import AutoLang from './components/autoLang.vue' 12 | import {policies} from './utils/store.js' 13 | import Google from './google/displayer.vue' 14 | import OpenGraph from './openGraph/displayer.vue' 15 | import Middlewares from './middlewares/displayer.vue' 16 | import _ from 'lodash' 17 | import {notify as urlChangedNotify} from './utils/urlChanged.js' 18 | 19 | module.exports = { 20 | policies, 21 | install (Vue, options = {}) { 22 | 23 | const titleChild = document.head.childNodes; 24 | const originalTitle = _.find(titleChild, (el) => { 25 | return el.tagName === 'TITLE'; 26 | }); 27 | if (originalTitle) { 28 | document.head.removeChild(originalTitle); 29 | } 30 | VueStore.Vue = Vue; 31 | _.merge(VueStore.options, { 32 | openGraph: true, 33 | html: true, 34 | schemaOrg: true, 35 | },options, { 36 | policies: { 37 | breadcrumbs: policies.identity, 38 | 'lang': policies.repeat, 39 | 'image': policies.repeat 40 | } 41 | }); 42 | 43 | Vue.component('seoTitle', Title); 44 | Vue.component('seoOrganization', Organization); 45 | Vue.component('seoBreadcrumb', Breadcrumb); 46 | Vue.component('seoCanonical', Canonical); 47 | Vue.component('seoImage', SeoImage); 48 | Vue.component('seoType', Type); 49 | Vue.component('seoLang', Lang); 50 | Vue.component('seoDescription', Description); 51 | Vue.component('seoAutoLang', AutoLang); 52 | 53 | Head.el = 'head' 54 | Head.comps.push(Vue.extend(Google)); 55 | Head.comps.push(Vue.extend(OpenGraph)); 56 | Head.comps.push(Vue.extend(Middlewares)); 57 | new Vue(Head); 58 | }, 59 | urlChangedNotify 60 | } 61 | -------------------------------------------------------------------------------- /src/middlewares/autoLangDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 76 | -------------------------------------------------------------------------------- /src/middlewares/displayer.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/mixins/displayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 62 | -------------------------------------------------------------------------------- /src/mixins/ldDisplayer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ready () { 3 | this.ldElem = document.createElement('script'); 4 | this.ldElem.type = 'application/ld+json'; 5 | document.head.appendChild(this.ldElem); 6 | const save = () => { 7 | this.ldElem.innerHTML = JSON.stringify(this.ld); 8 | } 9 | this.$watch('ld', { 10 | handler: save, 11 | deep: true 12 | }); 13 | save(); 14 | }, 15 | 16 | destroyed () { 17 | document.head.removeChild(this.ldElem); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/mixins/optionAccess.js: -------------------------------------------------------------------------------- 1 | import VueStore from '../utils/vueStore.js' 2 | 3 | export default { 4 | created () { 5 | this.seoOptions = VueStore.options; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/mixins/storeAccess.js: -------------------------------------------------------------------------------- 1 | var lastAccessorId = 0; 2 | 3 | import {set, remove} from '../utils/store.js' 4 | 5 | export default { 6 | created () { 7 | this._seoaid = lastAccessorId++; 8 | }, 9 | 10 | methods: { 11 | setInStore (key, value) { 12 | set(this._seoaid, key, value); 13 | }, 14 | removeFromStore (key) { 15 | remove(this._seoaid, key); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/mixins/writer.js: -------------------------------------------------------------------------------- 1 | import StoreAcess from './storeAccess.js' 2 | import _ from 'lodash' 3 | export default { 4 | mixins: [StoreAcess], 5 | 6 | ready () { 7 | _.each(this.keys, (values) => { 8 | _.each(values, (toWatch) => { 9 | this.$watch(toWatch, this.save); 10 | }); 11 | }); 12 | this.save(); 13 | }, 14 | 15 | methods: { 16 | save () { 17 | _.each(this.keys, (propsToSave, key) => { 18 | const data = {}; 19 | _.each(propsToSave, (prop) => { 20 | data[prop] = this[prop]; 21 | }); 22 | this.setInStore(key, data); 23 | }); 24 | } 25 | }, 26 | 27 | beforeDestroy () { 28 | _.each(this.keys, (props, key) => { 29 | this.removeFromStore(key); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/openGraph/descriptionDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/openGraph/displayer.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/openGraph/imageDisplayer.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | -------------------------------------------------------------------------------- /src/openGraph/langDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/openGraph/organizationDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/openGraph/titleDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /src/openGraph/typeDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/openGraph/urlDisplayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 20 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | const store = {}; 4 | var listener = null; 5 | 6 | const warnKeyChange = () => { 7 | if (typeof listener === 'function') { 8 | listener(_.keys(store)); 9 | } 10 | } 11 | 12 | export const firstPolicy = (data) => { 13 | return [_.first(data).value]; 14 | } 15 | 16 | const lastPolicy = (data) => { 17 | return [_.last(data).value]; 18 | } 19 | 20 | const joinPolicy = (separator, reversed = false) => { 21 | return (arr) => { 22 | if (arr.length === 0) return []; 23 | const ks = _.keys(arr[0].value); 24 | const values = _.map(ks, (key) => { 25 | const data = _.map(_.map(arr, 'value'), key); 26 | if (reversed) { 27 | data.reverse() 28 | } 29 | return data.join(separator); 30 | }); 31 | const zipped = [_.zipObject(ks, values)]; 32 | return zipped; 33 | } 34 | } 35 | 36 | const identityPolicy = (data) => { 37 | return [data]; 38 | } 39 | 40 | const repeatPolicy = (data) => { 41 | return _.map(data, 'value'); 42 | } 43 | 44 | const defaultPolicy = lastPolicy; 45 | 46 | export const policies = { 47 | last: lastPolicy, 48 | first: firstPolicy, 49 | join: joinPolicy, 50 | identity: identityPolicy, 51 | repeat: repeatPolicy 52 | } 53 | 54 | export const get = (key, policy = defaultPolicy) => { 55 | if (_.has(store, key) && store[key].length > 0) { 56 | return policy(store[key]); 57 | } else { 58 | return ""; 59 | } 60 | } 61 | 62 | export const set = (id, key, value) => { 63 | createKeyIfNotExist(key); 64 | const found = _.find(store[key], {id}); 65 | if (found) { 66 | found.value = value; 67 | } else { 68 | store[key].push({id, value}); 69 | } 70 | } 71 | 72 | export const remove = (id, key) => { 73 | if (_.has(store,key)) { 74 | const index = _.findIndex(store[key], {id}); 75 | store[key].splice(index, 1); 76 | } 77 | } 78 | 79 | const createKeyIfNotExist = (key) => { 80 | if ( ! _.has(store, key)) { 81 | store[key] = []; 82 | warnKeyChange(); 83 | } 84 | } 85 | 86 | export const toWatch = (key) => { 87 | return store[key]; 88 | } 89 | 90 | export const setKeysListener = (cb) => { 91 | listener = cb; 92 | } 93 | window.store = store; 94 | 95 | -------------------------------------------------------------------------------- /src/utils/urlChanged.js: -------------------------------------------------------------------------------- 1 | const listeners = []; 2 | 3 | import _ from 'lodash' 4 | 5 | export default (callback) => { 6 | window.addEventListener('hashchange', callback); 7 | window.addEventListener('popstate', callback); 8 | listeners.push(callback); 9 | } 10 | 11 | export const notify = () => { 12 | _.each(listeners, (listener) => { 13 | listener(); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/vueStore.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Vue: null, 3 | options: {} 4 | }; 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* vim: set softtabstop=2 shiftwidth=2 expandtab : */ 2 | var webpack = require('webpack'); 3 | var path = require('path') 4 | var _ = require('lodash') 5 | 6 | var baseConfig = { 7 | entry: './src/main.js', 8 | module: { 9 | loaders: [ 10 | { 11 | test: /\.vue$/, 12 | loader: 'vue' 13 | }, 14 | { 15 | test: /\.js$/, 16 | loader: 'babel', 17 | exclude: /node_modules/ 18 | }, 19 | { 20 | // edit this for additional asset file types 21 | test: /\.(png|jpg|gif)$/, 22 | loader: 'file?name=[name].[ext]?[hash]' 23 | } 24 | ], 25 | }, 26 | // example: if you wish to apply custom babel options 27 | // instead of using vue-loader's default: 28 | babel: { 29 | presets: ['es2015', 'stage-0'], 30 | plugins: ['transform-runtime', 'lodash'] 31 | }, 32 | output: { 33 | path: './', 34 | filename: "index.js", 35 | library: ["VueSEO"], 36 | libraryTarget: "umd" 37 | } 38 | }; 39 | 40 | /** 41 | * npm config allows vue-seo be distributed 42 | * as an npm package without double-requiring vue 43 | * */ 44 | 45 | module.exports = [ 46 | baseConfig, 47 | ]; 48 | 49 | 50 | if (process.env.NODE_ENV === 'production') { 51 | console.log('THIS IS PROD'); 52 | for (var i=0; i