├── .gitignore ├── seo_collection.coffee ├── seo_publications.coffee ├── package.js ├── versions.json ├── seo.coffee └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | 3 | /.idea/ 4 | -------------------------------------------------------------------------------- /seo_collection.coffee: -------------------------------------------------------------------------------- 1 | @SeoCollection = new Mongo.Collection('seo') -------------------------------------------------------------------------------- /seo_publications.coffee: -------------------------------------------------------------------------------- 1 | Meteor.publish 'seoByRouteName', (routeName) -> 2 | check(routeName, String) 3 | return SeoCollection.find({route_name: routeName}) -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "manuelschoebel:ms-seo", 3 | summary: "Easily config SEO for your routes", 4 | git: "https://github.com/DerMambo/ms-seo.git", 5 | version: "0.4.1" 6 | }); 7 | 8 | Package.onUse(function(api){ 9 | 10 | api.versionsFrom('1.0'); 11 | 12 | api.use(['mongo', 'coffeescript', 'underscore']); 13 | 14 | api.use([ 15 | 'jquery', 16 | 'deps', 17 | 'iron:router@1.0.0' 18 | ], 'client'); 19 | 20 | api.addFiles([ 21 | 'seo_collection.coffee' 22 | ]); 23 | 24 | // Client Files 25 | api.addFiles([ 26 | 'seo.coffee' 27 | ], 'client'); 28 | 29 | api.addFiles([ 30 | 'seo_publications.coffee' 31 | ], 'server'); 32 | }); 33 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "application-configuration", 5 | "1.0.3" 6 | ], 7 | [ 8 | "base64", 9 | "1.0.1" 10 | ], 11 | [ 12 | "binary-heap", 13 | "1.0.1" 14 | ], 15 | [ 16 | "blaze", 17 | "2.0.3" 18 | ], 19 | [ 20 | "blaze-tools", 21 | "1.0.1" 22 | ], 23 | [ 24 | "boilerplate-generator", 25 | "1.0.1" 26 | ], 27 | [ 28 | "callback-hook", 29 | "1.0.1" 30 | ], 31 | [ 32 | "check", 33 | "1.0.2" 34 | ], 35 | [ 36 | "coffeescript", 37 | "1.0.4" 38 | ], 39 | [ 40 | "ddp", 41 | "1.0.11" 42 | ], 43 | [ 44 | "deps", 45 | "1.0.5" 46 | ], 47 | [ 48 | "ejson", 49 | "1.0.4" 50 | ], 51 | [ 52 | "follower-livedata", 53 | "1.0.2" 54 | ], 55 | [ 56 | "geojson-utils", 57 | "1.0.1" 58 | ], 59 | [ 60 | "html-tools", 61 | "1.0.2" 62 | ], 63 | [ 64 | "htmljs", 65 | "1.0.2" 66 | ], 67 | [ 68 | "id-map", 69 | "1.0.1" 70 | ], 71 | [ 72 | "iron:controller", 73 | "1.0.0" 74 | ], 75 | [ 76 | "iron:core", 77 | "1.0.0" 78 | ], 79 | [ 80 | "iron:dynamic-template", 81 | "1.0.0" 82 | ], 83 | [ 84 | "iron:layout", 85 | "1.0.0" 86 | ], 87 | [ 88 | "iron:location", 89 | "1.0.0" 90 | ], 91 | [ 92 | "iron:middleware-stack", 93 | "1.0.0" 94 | ], 95 | [ 96 | "iron:router", 97 | "1.0.0" 98 | ], 99 | [ 100 | "iron:url", 101 | "1.0.0" 102 | ], 103 | [ 104 | "jquery", 105 | "1.0.1" 106 | ], 107 | [ 108 | "json", 109 | "1.0.1" 110 | ], 111 | [ 112 | "logging", 113 | "1.0.5" 114 | ], 115 | [ 116 | "meteor", 117 | "1.1.3" 118 | ], 119 | [ 120 | "minifiers", 121 | "1.1.2" 122 | ], 123 | [ 124 | "minimongo", 125 | "1.0.5" 126 | ], 127 | [ 128 | "mongo", 129 | "1.0.8" 130 | ], 131 | [ 132 | "observe-sequence", 133 | "1.0.3" 134 | ], 135 | [ 136 | "ordered-dict", 137 | "1.0.1" 138 | ], 139 | [ 140 | "random", 141 | "1.0.1" 142 | ], 143 | [ 144 | "reactive-dict", 145 | "1.0.4" 146 | ], 147 | [ 148 | "reactive-var", 149 | "1.0.3" 150 | ], 151 | [ 152 | "retry", 153 | "1.0.1" 154 | ], 155 | [ 156 | "routepolicy", 157 | "1.0.2" 158 | ], 159 | [ 160 | "spacebars", 161 | "1.0.3" 162 | ], 163 | [ 164 | "spacebars-compiler", 165 | "1.0.3" 166 | ], 167 | [ 168 | "templating", 169 | "1.0.9" 170 | ], 171 | [ 172 | "tracker", 173 | "1.0.3" 174 | ], 175 | [ 176 | "ui", 177 | "1.0.4" 178 | ], 179 | [ 180 | "underscore", 181 | "1.0.1" 182 | ], 183 | [ 184 | "webapp", 185 | "1.1.4" 186 | ], 187 | [ 188 | "webapp-hashing", 189 | "1.0.1" 190 | ] 191 | ], 192 | "pluginDependencies": [], 193 | "toolVersion": "meteor-tool@1.0.35", 194 | "format": "1.0" 195 | } -------------------------------------------------------------------------------- /seo.coffee: -------------------------------------------------------------------------------- 1 | SEO = 2 | settings: { 3 | title: '' 4 | rel_author: '' 5 | meta: [] 6 | og: [] 7 | twitter: [] 8 | ignore: 9 | meta: ['fragment'] 10 | link: ['stylesheet', 'icon', 'apple-touch-icon'] 11 | auto: 12 | twitter: true 13 | og: true 14 | set: ['description', 'url', 'title'] 15 | } 16 | 17 | # e.g. ignore('meta', 'fragment') 18 | ignore: (type, value) -> 19 | @settings.ignore[type].push(value) if @settings.ignore[type] and _.indexOf(@settings.ignore[type], value) is -1 20 | 21 | config: (settings) -> 22 | _.extend(@settings, settings) 23 | 24 | set: (options, clearBefore=true) -> 25 | @clearAll() if clearBefore 26 | 27 | currentRouter = Router.current() 28 | url = Router.url(currentRouter.route.getName(), currentRouter.params) if currentRouter 29 | #SEO.set({url: Router.url(currentRouter.route.name, currentRouter.params)}) 30 | 31 | meta = options.meta 32 | og = options.og 33 | link = options.link 34 | twitter = options.twitter 35 | 36 | @setTitle options.title if options.title 37 | 38 | if options.url 39 | @setUrl options.url 40 | else if url 41 | @setUrl url 42 | 43 | # set meta 44 | if meta and _.isArray(meta) 45 | for m in meta 46 | @setMeta("name='#{m.key}'", m.value) 47 | else if meta and _.isObject(meta) 48 | for k, v of meta 49 | @setMeta("name='#{k}'", v) 50 | 51 | # set og 52 | if og and _.isArray(og) 53 | for o in og 54 | @setMeta("property='og:#{o.key}'", o.value) 55 | else if og and _.isObject(og) 56 | for k, v of og 57 | @setMeta("property='og:#{k}'", v) 58 | 59 | # set link 60 | # as array {href: "...", rel: "..."} 61 | # or as object {rel: href} 62 | if link and _.isArray(link) 63 | for l in link 64 | @setLink(l.rel, l.href) 65 | else if link and _.isObject(link) 66 | for k, v of link 67 | @setLink(k, v) 68 | 69 | # set twitter 70 | if twitter and _.isArray(twitter) 71 | for o in twitter 72 | @setMeta("property='twitter:#{o.key}'", o.value) 73 | else if twitter and _.isObject(twitter) 74 | for k, v of twitter 75 | @setMeta("property='twitter:#{k}'", v) 76 | 77 | # set google+ rel author 78 | @setLink 'author', options.rel_author if options.rel_author 79 | 80 | clearAll: -> 81 | for m in $("meta") 82 | $m = $(m) 83 | # do not remove anything you do not control 84 | # MS Seo only sets metas with a name or property 85 | # Probably not the best solution 86 | controlled = $m.attr('name') or $m.attr('property') 87 | ignored = false 88 | if $m.attr('name') and _.indexOf(SEO.settings.ignore.meta, $m.attr('name')) > -1 89 | ignored = true 90 | else if $m.attr('property') and _.indexOf(SEO.settings.ignore.meta, $m.attr('property')) > -1 91 | ignored = true 92 | if not ignored and controlled 93 | $m.remove() 94 | for l in $("link") 95 | $l = $(l) 96 | controlled = $l.attr 'rel' 97 | $l.remove() if _.indexOf(SEO.settings.ignore.link, $l.attr('rel')) is -1 and controlled 98 | @set(@settings, false) 99 | @setTitle(@settings.title) 100 | 101 | setTitle: (title) -> 102 | document.title = title 103 | if _.indexOf(@settings.auto.set, 'title') isnt -1 104 | if @settings.auto.twitter 105 | @setMeta 'property="twitter:title"', title 106 | if @settings.auto.og 107 | @setMeta 'property="og:title"', title 108 | 109 | setUrl: (url) -> 110 | if _.indexOf(@settings.auto.set, 'url') isnt -1 111 | if @settings.auto.twitter 112 | @setMeta 'property="twitter:url"', url 113 | if @settings.auto.og 114 | @setMeta 'property="og:url"', url 115 | 116 | setLink: (rel, href, unique=true) -> 117 | @removeLink(rel) if unique 118 | if _.isArray(href) 119 | for h in href 120 | @setLink(rel, h, false) 121 | return 122 | 123 | if href 124 | $('head').append("") 125 | 126 | removeLink: (rel) -> 127 | $("link[rel='#{rel}']").remove() 128 | 129 | setMeta: (attr, content, unique=true) -> 130 | @removeMeta(attr) if unique 131 | if _.isArray(content) 132 | for v in content 133 | @setMeta(attr, v, false) 134 | return 135 | 136 | return unless content 137 | content = escapeHtmlAttribute(content) 138 | 139 | $('head').append("") 140 | 141 | name = attr.replace(/"|'/g, '').split('=')[1] 142 | if _.indexOf(@settings.auto.set, name) isnt -1 143 | if @settings.auto.twitter 144 | @setMeta "property='twitter:#{name}'", content 145 | if @settings.auto.og 146 | @setMeta "property='og:#{name}'", content 147 | 148 | removeMeta: (attr) -> 149 | $("meta[#{attr}]").remove() 150 | 151 | 152 | @SEO = SEO 153 | 154 | escapeHtmlAttribute = (string) -> 155 | return ("" + string).replace(/'/g, "'").replace(/"/g, """) 156 | 157 | getCurrentRouteName = -> 158 | router = Router.current() 159 | return unless router 160 | routeName = router.route.getName() 161 | return routeName 162 | 163 | # Get seo settings depending on route 164 | Deps.autorun( -> 165 | currentRouteName = getCurrentRouteName() 166 | return unless currentRouteName 167 | Meteor.subscribe('seoByRouteName', currentRouteName) 168 | ) 169 | 170 | # Set seo settings depending on route 171 | Deps.autorun( -> 172 | return unless SEO 173 | currentRouteName = getCurrentRouteName() 174 | settings = SeoCollection.findOne({route_name: currentRouteName}) or {} 175 | SEO.set(settings) 176 | ) 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | This package is no longer maintained 3 | 4 | 5 | 6 | ms-seo 7 | ====== 8 | 9 | An SEO helper package for Meteor.js. Originally posted as an article here: [manuel-schoebel.com/blog/meteor-and-seo](http://www.manuel-schoebel.com/blog/meteor-and-seo "Meteor.js and SEO") 10 | 11 | Installation 12 | ---- 13 | This package is on Atmosphere: 14 | 15 | meteor add manuelschoebel:ms-seo 16 | 17 | Configuration 18 | ---- 19 | You can set some standard values. This will be set if nothing else is available. 20 | 21 | ```js 22 | Meteor.startup(function() { 23 | if (Meteor.isClient) { 24 | return SEO.config({ 25 | title: 'Manuel Schoebel - MVP Development', 26 | meta: { 27 | 'description': 'Manuel Schoebel develops Minimal Viable Producs (MVP) for Startups' 28 | }, 29 | og: { 30 | 'image': 'http://manuel-schoebel.com/images/authors/manuel-schoebel.jpg' 31 | } 32 | }); 33 | } 34 | }); 35 | ``` 36 | 37 | As you can see, a meta tag in the head area is defined by a key and a value and it works the same way for the Open Graph 'og' tags. 38 | 39 | Static SEO Data 40 | ---- 41 | The SEO data for your static sites which do not have dynamic content are set in a collection called 'SeoCollection'. Every document must have a 'route_name' that relates to a named route of your Iron-Router routes. 42 | 43 | ```js 44 | SeoCollection.update( 45 | { 46 | route_name: 'aboutMe' 47 | }, 48 | { 49 | $set: { 50 | route_name: 'aboutMe', 51 | title: 'About - Manuel Schoebel', 52 | meta: { 53 | 'description': 'Manuel Schoebel is an experienced web developer and startup founder. He develops but also consults startups about internet topics.' 54 | }, 55 | og: { 56 | 'title': 'About - Manuel Schoebel', 57 | 'image': 'http://manuel-schoebel.com/images/authors/manuel-schoebel.jpg' 58 | } 59 | } 60 | }, 61 | { 62 | upsert: true 63 | } 64 | ); 65 | ``` 66 | 67 | If a route changes, the SEO package automatically fetches the new data from this collection and sets all tags. 68 | 69 | Dynamic SEO Data 70 | ---- 71 | Often times you want to set your SEO data dynamically, for example if you have a blog and you want that the documents title is equal to the blogposts title. You can do this easily in the Iron-Router after hook like this: 72 | 73 | ```js 74 | Router.map(function() { 75 | return this.route('blogPost', { 76 | path: '/blog/:slug', 77 | waitOn: function() { 78 | return [Meteor.subscribe('postFull', this.params.slug)]; 79 | }, 80 | data: function() { 81 | var post; 82 | post = Posts.findOne({ 83 | slug: this.params.slug 84 | }); 85 | return { 86 | post: post 87 | }; 88 | }, 89 | onAfterAction: function() { 90 | var post; 91 | // The SEO object is only available on the client. 92 | // Return if you define your routes on the server, too. 93 | if (!Meteor.isClient) { 94 | return; 95 | } 96 | post = this.data().post; 97 | SEO.set({ 98 | title: post.title, 99 | meta: { 100 | 'description': post.description 101 | }, 102 | og: { 103 | 'title': post.title, 104 | 'description': post.description 105 | } 106 | }); 107 | } 108 | }); 109 | }); 110 | ``` 111 | 112 | You can use the `SEO.set(object)` method and the object param looks the same as a document of the 'SeoCollection' but has no route_name. 113 | 114 | Rel Author for Google Authorship 115 | ---- 116 | You can configure google authorship easily with 117 | 118 | rel_author: 'https://www.google.com/+ManuelSchoebel' 119 | 120 | The output in your header will be the rel author link like this: 121 | 122 | 123 | 124 | You can use 'rel_author' in the configuration, SeoCollection entries or in SEO.set as well. 125 | 126 | ## Multiple Meta Tags 127 | For example for og:image you might want to have multiple image meta tags for one site. You can do this now by simply setting the og.image value to an array like this: 128 | 129 | SEO.set({ 130 | ... 131 | og: { 132 | 'image': ['http://www.your-domain.com/my-image-1.jpg', 'http://www.your-domain.com/my-image-2.jpg'] 133 | } 134 | }); 135 | 136 | // results in: 137 | 138 | 139 | 140 | ##Automatically set twitter and og meta tags like Title 141 | For a page you normally have exactly one title you want to use for the og:title and twitter:title meta tags. MS-SEO does this automatically for your title, url and descrption. For the description, just set the meta-description tag. 142 | 143 | You can also disable this in the settings: 144 | 145 | ```js 146 | Meteor.startup(function() { 147 | SEO.config({ 148 | ... 149 | auto: { 150 | twitter: false, 151 | og: true, 152 | set: ['description', 'url', 'title'] 153 | } 154 | }); 155 | }); 156 | ``` 157 | 158 | In this settings only the og metas are set automatically but not for twitter. The "set" array specifies what should be set. You could put any meta-tag in there and it will automatically be set for og or twitter as well. 159 | 160 | ## Using ignore 161 | You may run into a situation where you need to ignore certain tags (such as viewport meta tags). This can easily be done with MS-SEO by overwriting the standard ignore option: 162 | 163 | ```js 164 | Meteor.startup(function() { 165 | SEO.config({ 166 | ... 167 | ignore: { 168 | meta: ['fragment', 'viewport'], 169 | link: ['stylesheet', 'icon', 'apple-touch-icon'] 170 | } 171 | }); 172 | }); 173 | ``` 174 | 175 | Using this setting will cause MS-SEO to ignore all meta tags with 'viewport' in the name as well as the standard ignored tags. 176 | 177 | You Need More? 178 | ---- 179 | If you have different needs regarding meta tags and SEO, please [add a feature request](issues). 180 | --------------------------------------------------------------------------------