diff --git a/.meteor/packages b/.meteor/packages index 8c93b64fa..3f0c2d806 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -79,3 +79,5 @@ useraccounts:unstyled@1.4.0 telescope-datetimepicker tsega:bootstrap3-datetimepicker@3.1.3_1 +telescope-seo + diff --git a/.meteor/versions b/.meteor/versions index ed383c20e..2ae8bd2b3 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -36,6 +36,8 @@ facebook@1.1.2 fastclick@1.0.1 follower-livedata@1.0.2 fourseven:scss@1.0.0 +gadicohen:robots-txt@0.0.8 +gadicohen:sitemaps@0.0.20 geojson-utils@1.0.1 handlebars@1.0.1 html-tools@1.0.2 @@ -62,6 +64,7 @@ less@1.0.11 livedata@1.0.11 localstorage@1.0.1 logging@1.0.5 +manuelschoebel:ms-seo@0.4.1 matb33:collection-hooks@0.7.6 meteor-platform@1.2.0 meteor@1.1.3 @@ -125,6 +128,7 @@ telescope-newsletter@0.1.0 telescope-notifications@0.1.0 telescope-rss@0.0.0 telescope-search@0.0.0 +telescope-seo@0.0.4 telescope-singleday@0.1.0 telescope-tags@0.0.0 telescope-theme-base@0.0.0 diff --git a/packages/telescope-seo/i18n/en.i18n.json b/packages/telescope-seo/i18n/en.i18n.json new file mode 100644 index 000000000..a1349125a --- /dev/null +++ b/packages/telescope-seo/i18n/en.i18n.json @@ -0,0 +1,6 @@ +{ + "search_engine_optimization": "search engine optimization", + "seoDescription": "meta and og description", + "seoOgImage": "og:image", + "seoGenerateSitemap": "Generate Sitemap (requires restart)" +} diff --git a/packages/telescope-seo/lib/routes.js b/packages/telescope-seo/lib/routes.js new file mode 100644 index 000000000..8c2f6ab8a --- /dev/null +++ b/packages/telescope-seo/lib/routes.js @@ -0,0 +1,53 @@ +Meteor.startup(function() { + var addSeoTags = function(descriptionFn, canonicalUrlFn) { + var props = {link: {}, meta: {}, og: {}}; + var title = this.getTitle && this.getTitle(); + var description = descriptionFn.call(this); + var image = getSetting("seoOgImage"); + if (title) { + props.og.title = title; + } + if (description) { + props.meta.description = description; + props.og.description = description; + } + if (image) { + props.og.image = image; + } + if (canonicalUrlFn) { + props.link.canonical = canonicalUrlFn.call(this); + } + SEO.set(props); + }; + + // Front page: prefer description from settings over this.getDescription. + var frontPageDescription = function() { + return getSetting("seoDescription") || (this.getDescription && this.getDescription()); + }; + + // All others: prefer this.getDescription over settings. + var notFrontPageDescription = function() { + return (this.getDescription && this.getDescription()) || getSetting("seoDescription"); + }; + + var frontPage = ["posts_" + getSetting("frontPage", "top").toLowerCase()]; + var postPage = ["post_page", "post_page_with_slug"]; + + // Front page + Router.onAfterAction(function() { + addSeoTags.call(this, frontPageDescription); + }, {only: frontPage}); + + // Post detail pages + Router.onAfterAction(function() { + addSeoTags.call(this, notFrontPageDescription, function getCanonicalUrl() { + var post = Posts.findOne(this.params._id); + return getPostPageUrl(post); + }); + }, {only: postPage}); + + // All others + Router.onAfterAction(function() { + addSeoTags.call(this, notFrontPageDescription); + }, {except: frontPage.concat(postPage)}); +}); diff --git a/packages/telescope-seo/lib/seo.js b/packages/telescope-seo/lib/seo.js new file mode 100644 index 000000000..fa84f41cb --- /dev/null +++ b/packages/telescope-seo/lib/seo.js @@ -0,0 +1,39 @@ +// Add SEO settings. +addToSettingsSchema.push({ + propertyName: "seoDescription", + propertySchema: { + type: String, + optional: true, + label: "description", + autoform: { + group: "search engine optimization", + instructions: "Content for the meta description og:description tags for the front page and others that don't otherwise specify it.", + rows: 2 + } + } +}); +addToSettingsSchema.push({ + propertyName: "seoOgImage", + propertySchema: { + type: String, + optional: true, + regEx: SimpleSchema.RegEx.Url, + label: "og:image", + autoform: { + group: "search engine optimization", + instructions: "URL to an image for the open graph image tag for all pages" + } + } +}); +addToSettingsSchema.push({ + propertyName: "seoGenerateSitemap", + propertySchema: { + type: Boolean, + defaultValue: false, + label: "Generate sitemap", + autoform: { + group: "search engine optimization", + instructions: "Automatically generate an XML sitemap for search engines, and append the sitemap URL to the output of robots.txt? NOTE: Requires restart to reflect change." + } + } +}) diff --git a/packages/telescope-seo/lib/server/sitemaps.js b/packages/telescope-seo/lib/server/sitemaps.js new file mode 100644 index 000000000..6cfb31fe1 --- /dev/null +++ b/packages/telescope-seo/lib/server/sitemaps.js @@ -0,0 +1,58 @@ +Meteor.startup(function() { + /* + * Sitemap + */ + if (getSetting("seoGenerateSitemap")) { + sitemaps.add("/sitemap.xml", function() { + var _getLatest = function(viewParamKey, terms) { + var params = getPostsParameters( + viewParameters[viewParamKey.toLowerCase()](terms) + ); + var post = Posts.findOne(params.find, { + 'fields': {'postedAt': 1}, + 'sort': params.options.sort + }); + return post ? post.postedAt : null; + } + // Posts list pages + var paths = [ + {page: "/", lastmod: _getLatest(getSetting("defaultView", "top")), changefreq: "hourly"}, + {page: "/top", lastmod: _getLatest("top"), changefreq: "hourly"}, + {page: "/new", lastmod: _getLatest("new"), changefreq: "hourly"}, + {page: "/best", lastmod: _getLatest("best"), changefreq: "daily"}, + ]; + // Categories (if telescope-tags is included) + if (typeof Categories !== "undefined") { + Categories.find({}, {fields: {"slug": 1}}).forEach(function(category) { + var lastMod = _getLatest("category", {category: category.slug}); + if (lastMod) { + paths.push({ + page: "/category/" + category.slug, + lastmod: lastMod, + changefreq: "hourly" + }) + } + }); + } + // Individual post pages: include 100 latest in each of "top", "new", and + // "best". Aggregate them to avoid duplication. + var postPages = {}; + _.each(["top", "new", "best"], function(key) { + var siteUrl = getSiteUrl(); + var params = getPostsParameters(viewParameters[key]()); + var posts = Posts.find(params.find, { + fields: {postedAt: 1, title: 1, _id: 1}, + limit: 100, + sort: params.options.sort + }); + posts.forEach(function(post) { + var url = getPostPageUrl(post).replace(siteUrl, ""); + postPages[url] = {page: url, lastmod: post.postedAt, changefreq: "daily"}; + }); + }); + paths = paths.concat(_.values(postPages)); + paths = _.reject(paths, function(p) { return p.lastmod === null }); + return paths; + }); + } +}); diff --git a/packages/telescope-seo/package-tap.i18n b/packages/telescope-seo/package-tap.i18n new file mode 100644 index 000000000..7e604228b --- /dev/null +++ b/packages/telescope-seo/package-tap.i18n @@ -0,0 +1,5 @@ +{ + "translation_function_name": "__", + "helper_name": "_", + "namespace": "project" +} diff --git a/packages/telescope-seo/package.js b/packages/telescope-seo/package.js index ce74a54f4..2808880b5 100644 --- a/packages/telescope-seo/package.js +++ b/packages/telescope-seo/package.js @@ -1,24 +1,32 @@ Package.describe({ name: "telescope-seo", summary: "SEO extensions for Telescope", - version: "0.0.3" + version: "0.0.4" }); Package.onUse(function(api) { - api.use([ + "templating", "underscore", "aldeed:simple-schema", + "tap:i18n", "iron:router", "telescope-lib", "telescope-base", - "telescope-tags", + "telescope-i18n", "manuelschoebel:ms-seo@0.4.1", "gadicohen:sitemaps@0.0.20" ]); - api.export([ - ]); + // both + api.addFiles([ + "lib/routes.js", + "lib/seo.js", + "package-tap.i18n" + ], ['client', 'server']); - api.addFiles("seo.js", ['client', 'server']); + // server + api.addFiles([ + "lib/server/sitemaps.js" + ], ["server"]); }); diff --git a/packages/telescope-seo/seo.js b/packages/telescope-seo/seo.js deleted file mode 100644 index c98115268..000000000 --- a/packages/telescope-seo/seo.js +++ /dev/null @@ -1,190 +0,0 @@ -// Add SEO settings. -addToSettingsSchema.push({ - propertyName: "seoMetaDescription", - propertySchema: { - type: String, - optional: true, - label: "meta description", - autoform: { - group: "search engine optimization", - instructions: "Content for the meta description tag for the front page and others that don't otherwise specify it.", - rows: 2 - } - } -}); -addToSettingsSchema.push({ - propertyName: "seoOgDescription", - propertySchema: { - type: String, - optional: true, - label: "og:description", - autoform: { - group: "search engine optimization", - instructions: "Content for the open graph description tag for the front page and others that don't otherwise specify it.", - rows: 2 - } - } -}); -addToSettingsSchema.push({ - propertyName: "seoOgImage", - propertySchema: { - type: String, - optional: true, - regEx: SimpleSchema.RegEx.Url, - label: "og:image", - autoform: { - group: "search engine optimization", - instructions: "URL to an image for the open graph image tag for all pages" - } - } -}); -addToSettingsSchema.push({ - propertyName: "seoGenerateSitemap", - propertySchema: { - type: Boolean, - defaultValue: false, - label: "Generate sitemap", - autoform: { - group: "search engine optimization", - instructions: "Automatically generate an XML sitemap for search engines, and append the sitemap URL to the output of robots.txt? NOTE: Requires restart to reflect change." - } - } -}) - -if (Meteor.isClient) { - Meteor.startup(function() { - /* - * Meta tags - */ - - // Post pages - Router.onAfterAction(function() { - var post = Posts.findOne(this.params._id); - if (!post) { - return; - } - var title = (typeof this.getTitle === 'function') ? this.getTitle() : post.title; - if (post.categories && post.categories.length > 0) { - title += " - " + _.pluck(post.categories, "name").join(", "); - } - var stitle = getSetting("title"); - if (stitle) { - title += " - " + stitle; - } - var description = [getSetting("tagline"), post.title].join(" "); - SEO.set({ - link: { canonical: getPostPageUrl(post) }, - meta: { description: description }, - og: { - title: title, - description: description, - image: getSetting("seoOgImage") - } - }); - }, {only: ["post_page", "post_page_with_slug"]}); - - // User pages - Router.onAfterAction(function() { - var user = Meteor.users.findOne(this.params._idOrSlug); - if (user) { - var title; - if (typeof this.getTitle === 'function') { - title = this.getTitle(); - } else { - title = getUserName(user) + " - " + getSetting("title", ""); - } - var description = "User profile for " + getUserName(user) + " - " + getSetting("title"); - SEO.set({ - link: { canonical: getSiteUrl() + "users/" + user._id }, - meta: { description: description }, - og: { - title: title, - description: description, - image: getSetting("seoOgImage") - } - }); - } - }, {only: ["user_profile"]}); - - // All other pages - Router.onAfterAction(function() { - var title; - if (typeof this.getTitle === 'function') { - title = this.getTitle(); - } else { - var stitle = getSetting("title"); - var stagline = getSetting("tagline"); - title = (stagline ? stitle + ": " + stagline : stitle) || ""; - } - SEO.set({ - meta: {description: getSetting("seoMetaDescription")}, - og: { - title: title, - description: getSetting("seoOgDescription"), - image: getSetting("seoOgImage") - } - }); - }, {except: ["user_profile", "post_page", "post_page_with_slug"]}); - }); -} - -if (Meteor.isServer) { - Meteor.startup(function() { - /* - * Sitemap - */ - if (getSetting("seoGenerateSitemap")) { - sitemaps.add("/sitemap.xml", function() { - var _getLatest = function(viewParamKey, terms) { - var params = getPostsParameters( - viewParameters[viewParamKey.toLowerCase()](terms) - ); - var post = Posts.findOne(params.find, { - 'fields': {'postedAt': 1}, - 'sort': params.options.sort - }); - return post ? post.postedAt : null; - } - // Posts list pages - var paths = [ - {page: "/", lastmod: _getLatest(getSetting("defaultView", "top")), changefreq: "hourly"}, - {page: "/top", lastmod: _getLatest("top"), changefreq: "hourly"}, - {page: "/new", lastmod: _getLatest("new"), changefreq: "hourly"}, - {page: "/best", lastmod: _getLatest("best"), changefreq: "daily"}, - ]; - // Categories (if telescope-tags is included) - if (typeof Categories !== "undefined") { - Categories.find({}, {fields: {"slug": 1}}).forEach(function(category) { - var lastMod = _getLatest("category", {category: category.slug}); - if (lastMod) { - paths.push({ - page: "/category/" + category.slug, - lastmod: lastMod, - changefreq: "hourly" - }) - } - }); - } - // Individual post pages: include 100 latest in each of "top", "new", and - // "best". Aggregate them to avoid duplication. - var postPages = {}; - _.each(["top", "new", "best"], function(key) { - var siteUrl = getSiteUrl(); - var params = getPostsParameters(viewParameters[key]()); - var posts = Posts.find(params.find, { - fields: {postedAt: 1, title: 1, _id: 1}, - limit: 100, - sort: params.options.sort - }); - posts.forEach(function(post) { - var url = getPostPageUrl(post).replace(siteUrl, ""); - postPages[url] = {page: url, lastmod: post.postedAt, changefreq: "daily"}; - }); - }); - paths = paths.concat(_.values(postPages)); - paths = _.reject(paths, function(p) { return p.lastmod === null }); - return paths; - }); - } - }); -} diff --git a/packages/telescope-seo/versions.json b/packages/telescope-seo/versions.json index 963d1fcc0..3ffb036fd 100644 --- a/packages/telescope-seo/versions.json +++ b/packages/telescope-seo/versions.json @@ -204,10 +204,6 @@ "telescope-lib", "0.2.9" ], - [ - "telescope-tags", - "0.0.0" - ], [ "templating", "1.0.9" diff --git a/server/migrations.js b/server/migrations.js index 6ddbf49f1..65bbf7e5d 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -411,8 +411,9 @@ var migrationsList = { i++; console.log("Post: " + post._id); var justCategoryIds = post.categories.map(function (category){ - return category._id; + return category && category._id; }); + justCategoryIds = _.reject(justCategoryIds, function(id) { return id === null; }); var result = Posts.update(post._id, {$set: {categories: justCategoryIds, oldCategories: post.categories}}, {multi: true, validate: false}); console.log("---------------------"); }); @@ -430,4 +431,4 @@ var migrationsList = { } }; -// TODO: normalize categories? \ No newline at end of file +// TODO: normalize categories?