[Photo credit: [smartreachdigital.com](http://smartreachdigital.com/)]
- 1st commit: Oct 6th, 2013
- Number of commits: 498
- Total number of issues: 165
- Total number of PRs: 59
- Number of contributors: 27
- Stars: 366
- Watchers: 29
- Forks: 114
Those are the current GitHub stats for the ryw:blog Meteor package. A couple years ago, I wrote about why we created the Meteor Blog package. In short, we wanted to add a blog to some client sites, but didn't want to host a separate Meteor app to do so. Wouldn't it be cool if you could add a blog to your Meteor site by running a single command?
YES! It would be cool.
Turns out, building a blogging platform is hard. Building a blogging package is even harder. Meteor Blog presents some interesting technical challenges. The package isn't just a utility package. It's really an app within an app, and must handle getting installed in unknown & hostile environments. For example, as a blog, Meteor Blog definitely needs routing. But it would be installed to a host app that used either Iron Router or Flow Router. So it has to support both.
Let's look at some of this work.
Supporting Iron Router & Flow Router in a Meteor Package
Now, Meteor Blog originally depended on Iron Router for routing. Iron Router offered a lot of ways to conduct data and subscription logic at the route level. However, Flow Router eschews this pattern and in fact make it very difficult to handle subscription data in its routes. (I happen to agree with this. In fact, when I think back on some of my most frustrating moments with Meteor, it often had to do with a reactive route gone awry.) So the first step was to move all my subscription logic out of my routes and into my templates.
With subscriptions out of the way, my routes consisted of nothing more than names and templates. One solution was to simply create two routing files, one for each router, and maintain them both. The blog could detect which one to use by using the Package global. There had to be a better way.
And there was, I think. Ultimately, I created my own mini-router that abstracted away the actual router. For most URL-type work, I used the router-independent iron:url package (thank you Iron Meteor). For other tasks, I did a straight up branch between the two routers. For example, each router did replaceState-type transitions differently.
Blog.Router = {
replaceState: function(path) {
if (Package['iron:router']) {
Iron.Location.go(path, {
replaceState: true,
skipReactive: true
});
} else if (Package['kadira:flow-router']) {
FlowRouter.withReplaceState(function() {
FlowRouter.go(path);
});
}
}
};
But what about the routes?
Turns out that by keeping routes simple, all I really needed to do for a given route was know which template to render. The idea was that I could create a "catch-all" route for each router that intercepted every route change. I could inspect the route and see if it was a blog route. If it was, handle it. If it wasn't, pass it on to the router to handle like normal. In Flow Router's case, there was no concept of next(), so I just had to ensure the route ran last. It took me a few tries, but I ultimately ended up with this simple function.
Blog.Router = {
routeAll: function(routes) {
this.routes = routes;
if (Package['iron:router']) {
Package['iron:router'].Router.route('/(.*)', {
onBeforeAction: function() {
var template = Blog.Router.getTemplate();
if (template) {
this.render(template);
} else {
this.next();
}
},
action: function() {
this.next();
}
});
} else if (Package['kadira:flow-router']) {
Package['kadira:flow-router'].FlowRouter.route('/:any*', {
action: function() {
var template = Blog.Router.getTemplate();
if (template) {
BlazeLayout.render(template);
} else {
Blog.Router.notFound();
}
}
});
} else {
throw new Meteor.Error(500, 'Blog requires either iron:router or kadira:flow-router');
}
}
};
The routes argument is an array of route objects. Each route object had only a path and a name, which drove the template. Again, I had to ensure that Meteor Blog routes ran after the app routes, so I called the function in Meteor.startup.
Meteor.startup(function() {
var routes = [];
routes.push({
path: '/blog',
name: 'blogIndex'
});
routes.push({
path: '/blog/:slug',
name: 'blogShow'
});
// ...other routes
Blog.Router.routeAll(routes);
});
Note that I did try to use Meteor Router Layer. Router Layer is a great idea and a good start, but it did not do everything I needed.
Running The Meteor Blog At Any URL In Your App
Setting up routing in this way opened up, almost by accident, some new flexibilities such as custom layouts just for blog routes. But the biggest benefit, and the feature in this release that I'm the most excited about, is the ability to set custom base paths.
From the README:
You can customize the base path for the blog and for the blog admin area.
Blog.config({
// '/myBlog', '/myBlog/my-post', '/myBlog/tag/whatever', etc.
basePath: '/myBlog',
adminBasePath: '/myBlogAdmin'
});
If you set the basepath to /, blog posts will appear at the root path of your app (e.g. http://myapp.com/my-post). This means that the blog index page will be your home page, unless you override the route. This also means that Meteor Blog can function as a crude CMS. For more CMS-like features, create a Github issue!
Bring Your Own Templates
Template HTML and CSS were heavily modified during this release. Every CSS class was prefixed in order to avoid potential app-CSS conflicts. There were some stray Bootstrap classes in the public-facing templates, and those were taken out. Semantic tags were taken out to offer maximum flexibility. In fact, most of the HTML in the public templates are now plain DIVs! At this level, you can achieve a lot of customized look-and-feel by using only CSS.
But if CSS isn't enough, Meteor Blog offers more levels of customization. Ultimately, you can bring your own Blaze templates, and Meteor Blog will set some fields in your data context. How exactly? Blaze makes this dead simple, since you can pass a template multiple onCreated callbacks and each one will run. For data helpers, helpers are just a map. This is all it takes to inject data to a template's context.
// Provide data to custom templates, if any
Meteor.startup(function() {
if (Blog.settings.blogIndexTemplate) {
var customIndex = Blog.settings.blogIndexTemplate;
Template[customIndex].onCreated(getBlogDataContextFunction);
Template[customIndex].helpers
posts: getBlogPostsData()
blogReady: isBlogDataReady()
});
Digression: Open Source Is Hard
I am a big fan of open-source. A huge fan. The many benefits of open-source are well-described elsewhere, so I will say only that there have been some truly magical things created entirely from the good will of humans.
But look around GitHub and you will see a graveyard of open-source projects, packages, and apps. I'm not even talking about the unsuccessful ones. Because when a project gets truly successful, it starts to eat up a lot more time. And that's when they get abandoned. Note that I am not talking about open-source projects with financial backing, like Node or even Meteor. There is a great collaborative discussion around this topic forming in this repo.
There were times when I tried to let Meteor Blog go. I thought that no one used it. No one cared. But inevitably someone would open an issue or request an enhancement, and the Meteor Blog post-it got stuck on the refrigerator door of my mind once again. So I continue to tinker.
Try It Out!
You can find the Meteor Blog on GitHub at https://github.com/Differential/meteor-blog. You can find it on Atmosphere at https://atmospherejs.com/ryw/blog. Add it to your app by running a single command:
$ meteor add ryw:blog
You can see an example app running at http://blog-example.meteor.com/.