├── .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 |
--------------------------------------------------------------------------------