├── .npminclude ├── test ├── .pages │ ├── nometadata.md │ └── page.md └── pages.js ├── .gitignore ├── lib ├── reed-util.js ├── reed.js ├── redis-connection.js ├── filesystem-helper.js ├── file-processor.js ├── keymanager.js ├── pages-connector.js ├── pages.js ├── blog-connector.js └── blog.js ├── package.json ├── LICENSE ├── CHANGELOG.md └── README.md /.npminclude: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /test/.pages/nometadata.md: -------------------------------------------------------------------------------- 1 | This is text. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | articles/* 3 | test.js 4 | -------------------------------------------------------------------------------- /test/.pages/page.md: -------------------------------------------------------------------------------- 1 | Author: Some Guy 2 | 3 | This is text. 4 | -------------------------------------------------------------------------------- /lib/reed-util.js: -------------------------------------------------------------------------------- 1 | var S = require('string'); 2 | 3 | exports.isMarkdownFilename = function(filename) { 4 | return S(filename).endsWith('.md') || S(filename).endsWith('.markdown'); 5 | } 6 | 7 | exports.isMarkdown = function(filename, stats) { 8 | return stats.isFile() && (S(filename).endsWith('.md') || S(filename).endsWith('.markdown')); 9 | } 10 | -------------------------------------------------------------------------------- /lib/reed.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | events = require('events'), 3 | blog = require('./blog'), 4 | pages = require('./pages'); 5 | 6 | //default redis configuration. 7 | var cfg = { 8 | host: '127.0.0.1', 9 | port: 6379, 10 | password: null 11 | }; 12 | 13 | blog.configure(cfg); 14 | pages.configure(cfg); 15 | pages.on('error', function(err) { 16 | blog.emit('error', err); 17 | }); 18 | 19 | module.exports = blog; 20 | module.exports.pages = pages; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "ProjectMoon", 3 | "name": "reed", 4 | "email": "rei@thermetics.net", 5 | "description": "Redis + markdown blogging/website core", 6 | "tags": [ "redis", "blog" ], 7 | "version": "1.0.0", 8 | "homepage": "http://www.agnos.is/", 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/ProjectMoon/reed.git" 12 | }, 13 | "engines": { 14 | "node": ">= 0.8.0" 15 | }, 16 | "directories": { 17 | "lib": "./lib" 18 | }, 19 | "main": "./lib/reed.js", 20 | "dependencies": { 21 | "redis": "", 22 | "node-markdown": "", 23 | "async": "", 24 | "hound": "", 25 | "string": "" 26 | }, 27 | "devDependencies": { 28 | "vows": "" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/redis-connection.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | 3 | var connections = 0; 4 | 5 | //The redis connection. 6 | var client; 7 | var open = false; 8 | 9 | exports.open = function(cfg, callback) { 10 | connections++; 11 | //already open? 12 | if (typeof client !== 'undefined' && open) { 13 | return process.nextTick(function() { 14 | callback(null, client); 15 | }); 16 | } 17 | 18 | client = redis.createClient(cfg.port, cfg.host); 19 | open = true; 20 | 21 | //authentication may cause redis to fail 22 | //this ensures we see the problem if it occurs 23 | client.on('error', function(errMsg) { 24 | connections--; 25 | open = false; 26 | callback(errMsg); 27 | }); 28 | 29 | if (cfg.password) { 30 | //if we are to auth we need to wait on callback before 31 | //starting to do work against redis 32 | return client.auth(cfg.password, function (err) { 33 | if (err) return callback(err); 34 | callback(null, client); 35 | }); 36 | 37 | } 38 | else { 39 | //no auth, just start 40 | return process.nextTick(function() { 41 | callback(null, client); 42 | }); 43 | } 44 | } 45 | 46 | exports.close = function() { 47 | connections--; 48 | 49 | if (connections == 0) { 50 | client.quit(); 51 | open = false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Reed is Distributed under the MIT License 2 | 3 | Copyright (c) 2011 Jeff Hair 4 | Portions Copyright (c) 2010 Time Caswell 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /lib/filesystem-helper.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | async = require('async'), 4 | ru = require('./reed-util'); 5 | 6 | function FilesystemHelper(directory) { 7 | this.dir = directory; 8 | } 9 | 10 | FilesystemHelper.prototype.exists = function(filename, callback) { 11 | fs.exists(this.dir + filename, callback); 12 | } 13 | 14 | FilesystemHelper.prototype.readMarkdown = function(dir, callback) { 15 | fs.readdir(dir, function(err, files) { 16 | if (err) return callback(err); 17 | var self = this; 18 | var filenames = []; 19 | 20 | var tasks = []; 21 | files.forEach(function(file) { 22 | tasks.push(function(cb) { 23 | var fullpath = path.join(dir, file); 24 | fs.stat(fullpath, function(err, stats) { 25 | if (err) return callback(err); 26 | if (ru.isMarkdown(file, stats)) { 27 | filenames.push(file); 28 | } 29 | 30 | cb(null); 31 | }); 32 | }); 33 | }); 34 | 35 | async.parallel(tasks, function(err) { 36 | if (err) return callback(err); 37 | callback(null, filenames); 38 | }); 39 | }); 40 | } 41 | 42 | FilesystemHelper.prototype.remove = function(filename, callback) { 43 | fs.unlink(filename, function(err) { 44 | if (typeof callback !== 'undefined') callback(err); 45 | }); 46 | } 47 | 48 | FilesystemHelper.prototype.removeAll = function() { 49 | this.readMarkdown(this.dir, function(files) { 50 | files.forEach(function(filename) { 51 | this.remove(filename); 52 | }); 53 | }); 54 | } 55 | 56 | exports.FilesystemHelper = FilesystemHelper; 57 | -------------------------------------------------------------------------------- /lib/file-processor.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | parseMarkdown = require("node-markdown").Markdown; 3 | 4 | //taken from wheat -- MIT license 5 | function preProcess(markdown) { 6 | if (!(typeof markdown === 'string')) { 7 | markdown = markdown.toString(); 8 | } 9 | 10 | var props = {}; 11 | 12 | // Parse out headers 13 | var match; 14 | while(match = markdown.match(/^([a-z]+):\s*(.*)\s*\n/i)) { 15 | var name = match[1]; 16 | name = name[0].toLowerCase() + name.substring(1); 17 | var value = match[2]; 18 | markdown = markdown.substr(match[0].length); 19 | props[name] = value; 20 | } 21 | 22 | props.markdown = markdown; 23 | return props; 24 | } 25 | 26 | //Callback receives: 27 | // err 28 | // postDate - string version of the post date (from getTime()) 29 | // metadata 30 | // post content - the HTML 31 | exports.process = function(filename, callback) { 32 | fs.readFile(filename, function(err, data) { 33 | if (err) return callback(err); 34 | 35 | if (typeof data === 'undefined') { 36 | return callback(new Error('No data for ' + filename)); 37 | } 38 | 39 | var metadata = preProcess(data.toString()); 40 | var post = parseMarkdown(metadata.markdown); 41 | 42 | fs.stat(filename, function(err, stats) { 43 | if (err) return callback(err); 44 | var postDate = stats.mtime.getTime(); 45 | metadata.lastModified = postDate; 46 | callback(null, postDate, metadata, post); 47 | }); 48 | }); 49 | } 50 | 51 | exports.getLastModified = function(filename, callback) { 52 | fs.stat(filename, function(err, stats) { 53 | if (err) return callback(err); 54 | callback(null, stats.mtime); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /lib/keymanager.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | S = require('string') 3 | conn = require('./redis-connection'); 4 | 5 | //redis open/close methods 6 | function open(cfg, callback) { 7 | conn.open(cfg, function(err, redisClient) { 8 | if (err) return callback(err, false); 9 | client = redisClient; 10 | callback(err, false); 11 | }); 12 | } 13 | 14 | function close() { 15 | conn.close(); 16 | } 17 | 18 | //redis client 19 | var client; 20 | 21 | var keyManager = { 22 | blogIndex: 'reed:blog:index', 23 | blogNewIndex: 'reed:blog:newindex', 24 | blogDates: 'reed:blog:dates', 25 | pagesIndex: 'reed:pages:index', 26 | pagesNewIndex: 'reed:pages:newindex', 27 | 28 | open: open, 29 | close: close, 30 | 31 | toPostKeyFromFilename: function(filename) { 32 | if (!S(filename).startsWith('reed:blog:')) { 33 | return 'reed:blog:' + filename; 34 | } 35 | else { 36 | return filename; 37 | } 38 | }, 39 | 40 | toPagesKeyFromFilename: function(filename) { 41 | if (!S(filename).startsWith('reed:pages:')) { 42 | return 'reed:pages:' + filename; 43 | } 44 | else { 45 | return filename; 46 | } 47 | }, 48 | 49 | toPostFilenameFromTitle: function(title, callback) { 50 | client.get('reed:blogpointer:' + title, function(err, key) { 51 | if (err) return callback(err); 52 | 53 | if (typeof key == undefined || key == null) { 54 | return callback(new Error('Key does not exist.')); 55 | } 56 | 57 | callback(null, key); 58 | }); 59 | }, 60 | 61 | toPagesFilenameFromTitle: function(title, callback) { 62 | client.get('reed:pagespointer:' + title, function(err, key) { 63 | if (err) return callback(err); 64 | 65 | if (typeof key == undefined || key == null) { 66 | return callback(new Error('Key does not exist.')); 67 | } 68 | 69 | callback(null, key); 70 | }); 71 | }, 72 | 73 | toTitle: function(filename) { 74 | var ext = path.extname(filename); 75 | var title = path.basename(filename, ext); 76 | return title; 77 | }, 78 | 79 | toPostPointer: function(filename) { 80 | return 'reed:blogpointer:' + keyManager.toTitle(filename); 81 | }, 82 | 83 | toPagesPointer: function(filename) { 84 | return 'reed:pagespointer:' + keyManager.toTitle(filename); 85 | } 86 | }; 87 | 88 | exports.KeyManager = keyManager; 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | ===== 3 | 4 | * Ground up rewrite for node 0.8. Code is now much more maintainable, readable, 5 | and organized. 6 | * New architecture: reed -> APIs -> redis connector/filesystem helper/file 7 | processor. 8 | * No more blocking code in the library (not necessarily dependencies). 9 | * Articles no longer need to have dashes in the filename. 10 | * Reed will now watch files that end in ".markdown" as well as ".md". 11 | * Reed now properly detects file additions, updates, and removals that happened 12 | while it was not running. 13 | * `reed.index` and `reed.refresh` methods deprecated. They can still be called but will only 14 | emit a warning. They will be removed in the next version. 15 | * `reed.removeAll` method is now atomic (Redis MULTI). 16 | * Method blocking is now much more efficient by using a queue instead of what 17 | amounted to fancy spinlock. 18 | * `add`, `update`, and `remove` events for reed pages. 19 | 20 | 0.9.8 21 | ===== 22 | 23 | * Fixed bug where pages API would not open unless the blog portion wasn't open. 24 | * Fixed issue where pages would throw exceptions when creating Redis keys. 25 | * All `lastModified` values will now be exposed as Date objects. They are still 26 | stored as Unix timestamp strings in Redis. 27 | * The redis client will now only shut down if both pages and blog portions are 28 | closed. So, if both are open, close needs to be called on both to stop reed. 29 | * Removed all page-related events because they did not fire in the same manner 30 | as the blog portion. Pages are for a different purpose than blog anyway. 31 | * Added `reed.pages.remove` and `reed.pages.close` methods. 32 | * Added unit tests for Pages API, using Vows. 33 | * Added changelog file to the project to keep track of history. 34 | 35 | 0.9.6/0.9.7 36 | =========== 37 | 38 | * Fixed a bug in would cause the library to crash if there was no metadata 39 | defined in articles. 40 | * Added the ability to configure reed so that it can connect to redis running on 41 | different hosts/ports and use authentication. This is just a passthrough to 42 | the redis module's methods, so you can send in other options as well. 43 | 44 | 0.9.5 45 | ===== 46 | 47 | * Added `reed.all` to get all posts in the system, ordered by date. 48 | * Changed metadata properties to be camelCase instead of all lowercase. 49 | * Clarified in readme that reed will not have comments functionality any time soon. 50 | -------------------------------------------------------------------------------- /test/pages.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | events = require('events'), 4 | fs = require('fs'), 5 | reed = require('../lib/reed'); 6 | 7 | var dir = __dirname + '/.pages/'; 8 | vows.describe('Pages System').addBatch({ 9 | 'Open Pages,': { 10 | topic: function() { 11 | var self = this; 12 | reed.pages.open(dir, function(err) { 13 | assert.isNull(err); 14 | self.callback(); 15 | }); 16 | }, 17 | 18 | 'then': { 19 | 'get a page with metadata': { 20 | topic: function() { 21 | reed.pages.get('page', this.callback); 22 | }, 23 | 24 | 'is working correctly': function(err, metadata, htmlContent) { 25 | assert.isNull(err); 26 | assert.isNotNull(htmlContent); 27 | assert.isNotNull(metadata); 28 | }, 29 | 30 | 'lastModified is a date': function(err, metadata, htmlContent) { 31 | assert.instanceOf(metadata.lastModified, Date); 32 | }, 33 | 34 | 'post content is a string': function(err, metadata, htmlContent) { 35 | assert.isString(htmlContent); 36 | }, 37 | 38 | 'has metadata': function(err, metadata, htmlContent) { 39 | assert.isNotNull(metadata); 40 | assert.isObject(metadata); 41 | } 42 | }, 43 | 44 | 'get a page with no custom metadata': { 45 | topic: function() { 46 | reed.pages.get('nometadata', this.callback); 47 | }, 48 | 49 | 'is working correctly': function(err, metadata, htmlContent) { 50 | assert.isNull(err); 51 | assert.isNotNull(htmlContent); 52 | assert.isNotNull(metadata); 53 | }, 54 | 55 | 'lastModified is a date': function(err, metadata, htmlContent) { 56 | assert.instanceOf(metadata.lastModified, Date); 57 | }, 58 | 59 | 'post content is a string': function(err, metadata, htmlContent) { 60 | assert.isString(htmlContent); 61 | }, 62 | 63 | 'has metadata': function(err, metadata, htmlContent) { 64 | assert.isNotNull(metadata); 65 | assert.isObject(metadata); 66 | }, 67 | 68 | 'has no custom metadata': function(err, metadata, htmlContent) { 69 | var valid = true; 70 | for (var prop in metadata) { 71 | if (prop !== 'lastModified' && prop !== 'id' && prop !== 'markdown') { 72 | console.log('invalid property:', prop); 73 | valid = false; 74 | break; 75 | } 76 | } 77 | 78 | assert.isTrue(valid); 79 | } 80 | }, 81 | 82 | 'get a non-existant page': { 83 | topic: function() { 84 | reed.pages.get('does-not-exist', this.callback); 85 | }, 86 | 87 | 'is working correctly': function(err, metadata, htmlContent) { 88 | assert.isNotNull(err); 89 | assert.isUndefined(htmlContent); 90 | assert.isUndefined(metadata); 91 | } 92 | }, 93 | 94 | 'create a new page,': { 95 | topic: function() { 96 | var self = this; 97 | fs.writeFile(dir + 'newpage.md', 'This is a new page', function(err) { 98 | if (err) return self.callback(err); 99 | reed.pages.on('add', function(title) { 100 | reed.pages.get('newpage', self.callback); 101 | }); 102 | }); 103 | }, 104 | 105 | 'check for errors': function(err, metadata, htmlContent) { 106 | assert.isNull(err); 107 | }, 108 | 109 | 'delete the page': { 110 | topic: function() { 111 | reed.pages.remove('newpage', this.callback); 112 | }, 113 | 114 | 'check for removal errors': function(err) { 115 | assert.isNull(err); 116 | }, 117 | 118 | 'no longer on filesystem': function(err) { 119 | assert.isFalse(fs.existsSync(dir + 'newpage.md')); 120 | }, 121 | 122 | 'then check if removed from reed': { 123 | topic: function() { 124 | reed.pages.get('newpage', this.callback); 125 | }, 126 | 127 | 'is working correctly': function(err, metadata, htmlContent) { 128 | assert.isNotNull(err); 129 | assert.isUndefined(htmlContent); 130 | assert.isUndefined(metadata); 131 | } 132 | } 133 | }, 134 | 135 | 'close pages': { 136 | topic: function() { 137 | //simply here to make sure it doesn't hang or throw 138 | //exceptions. 139 | reed.pages.close(); 140 | } 141 | } 142 | } 143 | } 144 | }, 145 | }).export(module); 146 | -------------------------------------------------------------------------------- /lib/pages-connector.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | conn = require('./redis-connection'), 3 | fileProcessor = require('./file-processor'), 4 | keyManager = require('./keymanager').KeyManager; 5 | 6 | //the redis client 7 | var client; 8 | 9 | exports.open = function(cfg, callback) { 10 | keyManager.open(cfg, function(err) { 11 | if (err) return callback(err, false); 12 | conn.open(cfg, function(err, redisClient) { 13 | if (err) return callback(err, false); 14 | client = redisClient; 15 | callback(err, false); 16 | }); 17 | }); 18 | } 19 | 20 | exports.close = function() { 21 | conn.close(); 22 | keyManager.close(); 23 | } 24 | 25 | exports.getPageFilenameForTitle = function(title, callback) { 26 | keyManager.toPagesFilenameFromTitle(title, function(err, filename) { 27 | if (err) return callback(err); 28 | callback(null, filename); 29 | }); 30 | } 31 | 32 | exports.getPage = function(title, callback) { 33 | keyManager.toPagesFilenameFromTitle(title, function(err, filename) { 34 | if (err) return callback(err); 35 | exports.getPageByFilename(filename, callback); 36 | }); 37 | } 38 | 39 | exports.getPageByFilename = function(filename, callback) { 40 | var key = keyManager.toPagesKeyFromFilename(filename); 41 | 42 | client.hgetall(key, function(err, hash) { 43 | if (typeof(hash) !== 'undefined' && hash != null && Object.keys(hash).length > 0) { 44 | var post = hash.post; 45 | if (typeof callback !== "undefined") { 46 | var metadata = {}; 47 | try { 48 | metadata = JSON.parse(hash.metadata); 49 | } 50 | catch (parseErr) { 51 | //no good metadata - ignore 52 | } 53 | 54 | if (typeof metadata.lastModified !== 'undefined') { 55 | metadata.lastModified = new Date(metadata.lastModified); 56 | } 57 | 58 | callback(null, true, metadata, hash.post); 59 | } 60 | } 61 | else { 62 | callback(new Error('Page not found: ' + filename), false); 63 | } 64 | }); 65 | } 66 | 67 | exports.insertPage = function(filename, callback) { 68 | fileProcessor.process(filename, function(err, postDate, metadata, post) { 69 | if (err) return callback(err); 70 | 71 | var ptr = keyManager.toPagesPointer(filename); 72 | var key = keyManager.toPagesKeyFromFilename(filename); 73 | var title = keyManager.toTitle(filename); 74 | 75 | metadataString = JSON.stringify(metadata); 76 | client.sadd(keyManager.pagesIndex, filename, function(err) { 77 | client.set(ptr, filename, function(err) { 78 | client.hset(key, 'metadata', metadataString, function() { 79 | client.hset(key, 'post', post, callback); 80 | }); 81 | }); 82 | }); 83 | }); 84 | } 85 | 86 | exports.updatePage = function(filename, callback) { 87 | exports.insertPage(filename, callback); 88 | } 89 | 90 | exports.removePage = function(filename, callback) { 91 | var ptr = keyManager.toPagesPointer(filename); 92 | var key = keyManager.toPagesKeyFromFilename(filename); 93 | var title = keyManager.toTitle(filename); 94 | 95 | client.del(ptr, function(err) { 96 | if (err) return callback(err); 97 | 98 | client.del(key, function(err) { 99 | if (err) return callback(err); 100 | 101 | client.srem(keyManager.pagesIndex, filename, function(err) { 102 | if (err) return callback(err); 103 | callback(null, filename); 104 | }); 105 | }); 106 | }); 107 | } 108 | 109 | exports.cleanupPages = function(newIndex, callback) { 110 | var t1 = [], t2 = []; 111 | 112 | //create a temporary "new index" set in redis. 113 | newIndex.forEach(function(value) { 114 | t1.push(function(cb) { 115 | client.sadd(keyManager.pagesNewIndex, value, cb); 116 | }); 117 | }); 118 | 119 | async.parallel(t1, function(err) { 120 | if (err) return callback(err); 121 | 122 | client.sdiff(keyManager.pagesIndex, keyManager.pagesNewIndex, function(err, removedFilenames) { 123 | if (err) return callback(err); 124 | 125 | //remove all deleted keys from the index and system. 126 | removedFilenames.forEach(function(filename) { 127 | t2.push(function(cb) { 128 | exports.removePage(filename, function(err) { 129 | if (err) cb(err); 130 | client.srem(keyManager.pagesIndex, filename, cb); 131 | }); 132 | }); 133 | }); 134 | 135 | async.parallel(t2, function(err) { 136 | if (err) return callback(err); 137 | 138 | client.del(keyManager.pagesNewIndex, function(err) { 139 | if (err) return callback(err); 140 | callback(null, removedFilenames); 141 | }); 142 | }); 143 | }); 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /lib/pages.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | events = require('events'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | async = require('async'), 6 | hound = require('hound'), 7 | redis = require('./pages-connector'), 8 | keyManager = require('./keymanager').KeyManager, 9 | ru = require('./reed-util'), 10 | FilesystemHelper = require('./filesystem-helper').FilesystemHelper; 11 | 12 | //singleton to enable events. 13 | //user code interacts with it through exports.on method. 14 | function ReedPages() { } 15 | util.inherits(ReedPages, events.EventEmitter); 16 | 17 | var pages = new ReedPages(); 18 | 19 | //constants 20 | var upsertResult = redis.upsertResult; 21 | 22 | //directory to watch 23 | var dir; 24 | 25 | //redis configuration (default to start, but can change) 26 | //set by parent reed module. 27 | var cfg = {}; 28 | 29 | //states 30 | var open = false; 31 | var ready = false; 32 | 33 | var fsh; 34 | 35 | //methods queued because the connection isn't open 36 | var queue = []; 37 | 38 | function watch() { 39 | watcher = hound.watch(dir); 40 | 41 | watcher.on('create', function(filename, stats) { 42 | if (ru.isMarkdown(filename, stats)) { 43 | filename = path.resolve(process.cwd(), filename); 44 | redis.insertPage(filename, function(err) { 45 | if (err) return pages.emit('error', err); 46 | pages.emit('add', keyManager.toTitle(filename)); 47 | }); 48 | } 49 | }); 50 | 51 | watcher.on('change', function(filename, stats) { 52 | if (ru.isMarkdown(filename, stats)) { 53 | filename = path.resolve(process.cwd(), filename); 54 | redis.updatePage(filename, function(err) { 55 | if (err) return pages.emit('error', err); 56 | pages.emit('update', keyManager.toTitle(filename)); 57 | }); 58 | } 59 | }); 60 | 61 | watcher.on('delete', function(filename) { 62 | if (ru.isMarkdownFilename(filename)) { 63 | filename = path.resolve(process.cwd(), filename); 64 | redis.removePage(filename, function(err) { 65 | if (err) return pages.emit('error', err); 66 | pages.emit('remove', keyManager.toTitle(filename)); 67 | }); 68 | } 69 | }); 70 | } 71 | 72 | function initDirectory(callback) { 73 | fsh.readMarkdown(dir, function(err, files) { 74 | if (err) return callback(err); 75 | 76 | var newIndex = []; 77 | var tasks = []; 78 | files.forEach(function(filename) { 79 | tasks.push(function(cb) { 80 | var fullpath = path.resolve(dir, filename); 81 | redis.insertPage(fullpath, function(err, result) { 82 | if (err) cb(err); 83 | newIndex.push(fullpath); 84 | cb(null); 85 | }); 86 | }); 87 | }); 88 | 89 | async.parallel(tasks, function(err) { 90 | if (err) return callback(err); 91 | 92 | redis.cleanupPages(newIndex, function(err, removedFilenames) { 93 | if (err) return callback(err); 94 | callback(null); 95 | }); 96 | }); 97 | }); 98 | } 99 | 100 | pages.configure = function(config) { 101 | //selectively overwrite default config properties. this way the user 102 | //only needs to override what's necessary. 103 | for (prop in config) { 104 | if (config[prop]) { 105 | cfg[prop] = config[prop]; 106 | } 107 | } 108 | } 109 | 110 | pages.open = function(directory, callback) { 111 | if (open === true || ready === true) { 112 | throw new Error('reed pages already open on ' + dir); 113 | } 114 | 115 | if (typeof directory !== 'string') { 116 | throw new Error('Must specify directory to read from'); 117 | } 118 | 119 | dir = directory; 120 | fsh = new FilesystemHelper(directory); 121 | 122 | redis.open(cfg, function(err, success) { 123 | if (err) return callback(err); 124 | 125 | fsh = new FilesystemHelper(directory); 126 | initDirectory(function(err) { 127 | if (err) return pages.emit('error', err); 128 | watch(); 129 | open = true; 130 | ready = true; 131 | 132 | //handle any queued method calls. 133 | queue.forEach(function(queuedCall) { 134 | queuedCall(); 135 | }); 136 | 137 | queue = []; 138 | callback(null); 139 | }); 140 | }); 141 | } 142 | 143 | pages.close = function() { 144 | if (!open || !ready) { 145 | throw new Error('reed pages is not open.'); 146 | } 147 | 148 | redis.close(); 149 | open = false; 150 | ready = false; 151 | queue = []; 152 | } 153 | 154 | pages.get = function(title, callback) { 155 | if (!open) return queue.push(function() { 156 | pages.get(title, callback); 157 | }); 158 | 159 | redis.getPage(title, function(err, found, metadata, page) { 160 | if (err) return callback(err); 161 | 162 | if (found) { 163 | callback (null, metadata, page); 164 | } 165 | else { 166 | callback(new Error('Could not find page: ' + title)); 167 | } 168 | }); 169 | } 170 | 171 | pages.remove = function(title, callback) { 172 | if (!open) return queue.push(function() { 173 | pages.remove(title, callback); 174 | }); 175 | 176 | keyManager.toPagesFilenameFromTitle(title, function(err, filename) { 177 | if (err) return callback(err); 178 | 179 | redis.removePage(title, function(err) { 180 | if (err) return callback(err); 181 | 182 | fsh.remove(filename, function(err) { 183 | callback(err); 184 | }); 185 | }); 186 | }); 187 | } 188 | 189 | //Export 190 | module.exports = pages; 191 | -------------------------------------------------------------------------------- /lib/blog-connector.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | conn = require('./redis-connection'), 3 | fileProcessor = require('./file-processor'), 4 | keyManager = require('./keymanager').KeyManager; 5 | 6 | //the redis client 7 | var client; 8 | 9 | //constants 10 | exports.upsertResult = { 11 | UPDATE: 0, 12 | NEW: 1, 13 | NONE: 2 14 | }; 15 | 16 | /* 17 | * Open a connection to redis. 18 | * 19 | * cfg - the configuration to use. 20 | * callback - callback receives (error, success). success is true if the connection was 21 | * opened or is already open. 22 | */ 23 | exports.open = function(cfg, callback) { 24 | keyManager.open(cfg, function(err) { 25 | if (err) return callback(err); 26 | conn.open(cfg, function(err, redisClient) { 27 | if (err) return callback(err, false); 28 | client = redisClient; 29 | callback(err, false); 30 | }); 31 | }); 32 | } 33 | 34 | exports.close = function() { 35 | conn.close(); 36 | keyManager.close(); 37 | } 38 | 39 | exports.listPosts = function(callback) { 40 | client.zrevrange(keyManager.blogDates, 0, -1, function(err, titles) { 41 | if (err) return callback(err); 42 | callback(null, titles); 43 | }); 44 | } 45 | 46 | exports.getPost = function(title, callback) { 47 | keyManager.toPostFilenameFromTitle(title, function(err, filename) { 48 | if (err) return callback(err); 49 | if (filename == null) return callback(new Error('Post not found: ' + title)); 50 | exports.getPostByFilename(filename, callback); 51 | }); 52 | } 53 | 54 | exports.getPostByFilename = function(filename, callback) { 55 | var key = keyManager.toPostKeyFromFilename(filename); 56 | 57 | client.hgetall(key, function(err, hash) { 58 | if (typeof hash !== "undefined" && hash != null && Object.keys(hash).length > 0) { 59 | var post = hash.post; 60 | var metadata = {}; 61 | try { 62 | metadata = JSON.parse(hash.metadata); 63 | } 64 | catch (parseErr) { 65 | //no good metadata - ignore 66 | } 67 | 68 | if (typeof metadata.lastModified !== 'undefined') { 69 | metadata.lastModified = new Date(metadata.lastModified); 70 | } 71 | 72 | callback(null, true, metadata, hash.post); 73 | } 74 | else { 75 | callback(new Error('Post not found: ' + filename), false); 76 | } 77 | }); 78 | } 79 | 80 | exports.insertPost = function(filename, callback) { 81 | fileProcessor.process(filename, function(err, postDate, metadata, post) { 82 | if (err) return callback(err); 83 | 84 | var ptr = keyManager.toPostPointer(filename); 85 | var key = keyManager.toPostKeyFromFilename(filename); 86 | var title = keyManager.toTitle(filename); 87 | 88 | metadataString = JSON.stringify(metadata); 89 | client.sadd(keyManager.blogIndex, filename, function(err) { 90 | client.zadd(keyManager.blogDates, postDate, title, function(err) { 91 | client.set(ptr, filename, function(err) { 92 | client.hset(key, 'metadata', metadataString, function() { 93 | client.hset(key, 'post', post, callback); 94 | }); 95 | }); 96 | }); 97 | }); 98 | }); 99 | } 100 | 101 | exports.upsertPost = function(filename, callback) { 102 | var returnValue; 103 | exports.getPostByFilename(filename, function(err, found, metadata, post) { 104 | if (found) { 105 | //compare last modified times. 106 | fileProcessor.getLastModified(filename, function(err, lastModified) { 107 | if (err) return callback(err); 108 | 109 | if (lastModified.getTime() > metadata.lastModified.getTime()) { 110 | exports.updatePost(filename, function(err) { 111 | if (err) return callback(err); 112 | callback(null, exports.upsertResult.UPDATE); 113 | }); 114 | } 115 | else { 116 | //no need to do anything at all. 117 | process.nextTick(function() { 118 | callback(null, exports.upsertResult.NONE); 119 | }); 120 | } 121 | }); 122 | } 123 | else { 124 | //brand new. 125 | exports.insertPost(filename, function(err) { 126 | if (err) return callback(err); 127 | callback(null, exports.upsertResult.NEW); 128 | }); 129 | } 130 | }); 131 | } 132 | 133 | exports.updatePost = function(filename, callback) { 134 | //for now this can delegate to insert since redis does insert/overwrite. 135 | //might need it later if there need to be special rules for updates. 136 | exports.insertPost(filename, callback); 137 | } 138 | 139 | exports.removePost = function(filename, callback) { 140 | var ptr = keyManager.toPostPointer(filename); 141 | var key = keyManager.toPostKeyFromFilename(filename); 142 | var title = keyManager.toTitle(filename); 143 | 144 | client.del(ptr, function(err) { 145 | if (err) return callback(err); 146 | 147 | client.del(key, function(err) { 148 | if (err) return callback(err); 149 | 150 | client.zrem(keyManager.blogDates, title, function(err) { 151 | if (err) return callback(err); 152 | 153 | client.srem(keyManager.blogIndex, filename, function(err) { 154 | if (err) return callback(err); 155 | callback(null, filename); 156 | }); 157 | }); 158 | }); 159 | }); 160 | } 161 | 162 | exports.removePostByTitle = function(title, callback) { 163 | keyManager.toPostFilenameFromTitle(title, function(err, filename) { 164 | if (err) return callback(err); 165 | exports.removePost(filename, callback); 166 | }); 167 | } 168 | 169 | exports.removeAllPosts = function(callback) { 170 | exports.listPosts(function(err, titles) { 171 | if (err) return callback(err); 172 | 173 | //stuff that's easy to delete. 174 | var tran = client.multi(); 175 | tran.del(keyManager.blogDates); 176 | tran.del(keyManager.blogIndex); 177 | tran.del(keyManager.blogNewIndex); 178 | 179 | //Need to acquire all of the post filenames asyncly from redis, 180 | //so use the async library to (readably) get them all into the multi. 181 | var tasks = []; 182 | titles.forEach(function(title) { 183 | tasks.push(function(cb) { 184 | keyManager.toPostFilenameFromTitle(title, function(err, filename) { 185 | var key = keyManager.toPostKeyFromFilename(filename); 186 | var ptr = keyManager.toPostPointer(filename); 187 | tran.del(key); 188 | tran.del(ptr); 189 | cb(err); 190 | }); 191 | }); 192 | }); 193 | 194 | async.parallel(tasks, function(err) { 195 | if (err) return callback(err); 196 | tran.exec(function(err, replies) { 197 | if (err) return callback(err); 198 | callback(null); 199 | }); 200 | }); 201 | }); 202 | } 203 | 204 | exports.cleanup = function(newIndex, callback) { 205 | var t1 = [], t2 = []; 206 | 207 | //create a temporary "new index" set in redis. 208 | newIndex.forEach(function(value) { 209 | t1.push(function(cb) { 210 | client.sadd(keyManager.blogNewIndex, value, cb); 211 | }); 212 | }); 213 | 214 | async.parallel(t1, function(err) { 215 | if (err) return callback(err); 216 | 217 | client.sdiff(keyManager.blogIndex, keyManager.blogNewIndex, function(err, removedFilenames) { 218 | if (err) return callback(err); 219 | 220 | //remove all deleted keys from the index and system. 221 | removedFilenames.forEach(function(filename) { 222 | t2.push(function(cb) { 223 | exports.removePost(filename, function(err) { 224 | if (err) cb(err); 225 | client.srem(keyManager.blogIndex, filename, cb); 226 | }); 227 | }); 228 | }); 229 | 230 | async.parallel(t2, function(err) { 231 | if (err) return callback(err); 232 | 233 | client.del(keyManager.blogNewIndex, function(err) { 234 | if (err) return callback(err); 235 | callback(null, removedFilenames); 236 | }); 237 | }); 238 | }); 239 | }); 240 | } 241 | -------------------------------------------------------------------------------- /lib/blog.js: -------------------------------------------------------------------------------- 1 | var hound = require('hound'), 2 | events = require('events'), 3 | util = require('util'), 4 | path = require('path'), 5 | async = require('async'), 6 | ru = require('./reed-util'), 7 | redis = require('./blog-connector'), 8 | keyManager = require('./keymanager').KeyManager, 9 | FilesystemHelper = require('./filesystem-helper').FilesystemHelper; 10 | 11 | //import the page connector constants for ease of use. 12 | var upsertResult = redis.upsertResult; 13 | 14 | //singleton to enable events. 15 | //user code interacts with it through exports.on method. 16 | function ReedBlog() { } 17 | util.inherits(ReedBlog, events.EventEmitter); 18 | 19 | var blog = new ReedBlog(); 20 | 21 | //redis configuration (default to start, but can change) 22 | //set by parent reed module. 23 | var cfg = {}; 24 | 25 | //directory to watch 26 | var dir; 27 | 28 | //misc objects: file watcher, filesystem helper, etc. 29 | var watcher; 30 | var fsh; 31 | 32 | //states 33 | var open = false; 34 | var ready = false; 35 | 36 | //method queue for when methods are called without the redis connection open. 37 | var queue = []; 38 | 39 | //Private methods. 40 | function watch() { 41 | watcher = hound.watch(dir); 42 | 43 | watcher.on('create', function(filename, stats) { 44 | if (ru.isMarkdown(filename, stats)) { 45 | filename = path.resolve(process.cwd(), filename); 46 | redis.insertPost(filename, function(err) { 47 | if (err) return blog.emit('error', err); 48 | var title = keyManager.toTitle(filename); 49 | blog.emit('add', title); 50 | }); 51 | } 52 | }); 53 | 54 | watcher.on('change', function(filename, stats) { 55 | if (ru.isMarkdown(filename, stats)) { 56 | filename = path.resolve(process.cwd(), filename); 57 | console.log(filename); 58 | redis.updatePost(filename, function(err) { 59 | if (err) return blog.emit('error', err); 60 | var title = keyManager.toTitle(filename); 61 | blog.emit('update', title); 62 | }); 63 | } 64 | }); 65 | 66 | watcher.on('delete', function(filename) { 67 | if (ru.isMarkdownFilename(filename)) { 68 | filename = path.resolve(process.cwd(), filename); 69 | redis.removePost(filename, function(err) { 70 | if (err) return blog.emit('error', err); 71 | blog.emit('remove', filename); 72 | }); 73 | } 74 | }); 75 | } 76 | 77 | function initDirectory(callback) { 78 | fsh.readMarkdown(dir, function(err, files) { 79 | if (err) return callback(err); 80 | 81 | var newIndex = []; 82 | var tasks = []; 83 | files.forEach(function(filename) { 84 | tasks.push(function(cb) { 85 | var fullpath = path.resolve(dir, filename); 86 | redis.upsertPost(fullpath, function(err, result) { 87 | if (err) cb(err); 88 | if (result == upsertResult.NEW) { 89 | var title = keyManager.toTitle(filename); 90 | blog.emit('add', title); 91 | } 92 | else if (result == upsertResult.UPDATE) { 93 | var title = keyManager.toTitle(filename); 94 | blog.emit('update', title); 95 | } 96 | 97 | newIndex.push(fullpath); 98 | cb(null); 99 | }); 100 | }); 101 | }); 102 | 103 | async.parallel(tasks, function(err) { 104 | if (err) return callback(err); 105 | 106 | redis.cleanup(newIndex, function(err, removedFilenames) { 107 | if (err) return callback(err); 108 | 109 | removedFilenames.forEach(function(filename) { 110 | blog.emit('remove', filename); 111 | }); 112 | 113 | callback(null); 114 | }); 115 | }); 116 | }); 117 | } 118 | 119 | //Connection methods. 120 | blog.configure = function(config) { 121 | //selectively overwrite default config properties. this way the user 122 | //only needs to override what's necessary. 123 | for (prop in config) { 124 | if (config[prop]) { 125 | cfg[prop] = config[prop]; 126 | } 127 | } 128 | } 129 | 130 | blog.open = function(directory) { 131 | if (open === true || ready === true) { 132 | throw new Error('reed already open on ' + dir); 133 | } 134 | 135 | if (typeof directory !== 'string') { 136 | throw new Error('Must specify directory to read from'); 137 | } 138 | 139 | dir = directory; 140 | 141 | redis.open(cfg, function(err, success) { 142 | if (err) return blog.emit('error', err); 143 | 144 | fsh = new FilesystemHelper(directory); 145 | initDirectory(function(err) { 146 | if (err) return blog.emit('error', err); 147 | watch(); 148 | open = true; 149 | ready = true; 150 | 151 | //handle any queued method calls. 152 | queue.forEach(function(queuedCall) { 153 | queuedCall(); 154 | }); 155 | 156 | queue = []; 157 | blog.emit('ready'); 158 | }); 159 | }); 160 | } 161 | 162 | blog.close = function() { 163 | if (!open || !ready) { 164 | throw new Error('reed is not open.'); 165 | } 166 | 167 | redis.close(); 168 | watcher.clear(); 169 | ready = false; 170 | open = false; 171 | queue = []; 172 | } 173 | 174 | //Data manipulation methods. 175 | blog.get = function(title, callback) { 176 | if (!open) return queue.push(function() { 177 | blog.get(title, callback); 178 | }); 179 | 180 | redis.getPost(title, function(err, found, metadata, post) { 181 | if (err) return callback(err); 182 | 183 | if (found) { 184 | callback(null, metadata, post); 185 | } 186 | else { 187 | callback(new Error('Could not find post: ' + title)); 188 | } 189 | }); 190 | } 191 | 192 | blog.getMetadata = function(title, callback) { 193 | if (!open) return queue.push(function() { 194 | blog.getMetadata(title, callback); 195 | }); 196 | 197 | blog.get(title, function(err, metadata, post) { 198 | if (err) return callback(err); 199 | callback(null, metadata); 200 | }); 201 | } 202 | 203 | blog.all = function(callback) { 204 | if (!open) return queue.push(function() { 205 | blog.all(callback); 206 | }); 207 | 208 | blog.list(function(err, titles) { 209 | if (err) return callback(err); 210 | 211 | //create the series to load all posts asyncly in order. 212 | var getAllPosts = []; 213 | titles.forEach(function(title) { 214 | getAllPosts.push(function(cb) { 215 | blog.get(title, function(err, metadata, htmlContent) { 216 | var post = { 217 | metadata: metadata, 218 | htmlContent: htmlContent 219 | }; 220 | 221 | cb(err, post); 222 | }); 223 | }); 224 | }); 225 | 226 | //get all the posts. 227 | async.series(getAllPosts, function(err, posts) { 228 | callback(null, posts); 229 | }); 230 | }); 231 | } 232 | 233 | blog.list = function(callback) { 234 | if (!open) return queue.push(function() { 235 | blog.list(callback); 236 | }); 237 | 238 | redis.listPosts(function(err, titles) { 239 | if (err) return callback(err); 240 | callback(null, titles); 241 | }); 242 | } 243 | 244 | blog.remove = function(title, callback) { 245 | if (!open) return queue.push(function() { 246 | blog.remove(title, callback); 247 | }); 248 | 249 | redis.removePostByTitle(title, function(err, filename) { 250 | if (err) return callback(err); 251 | 252 | fsh.remove(filename, function(err) { 253 | if (err) return callback(err); 254 | callback(null); 255 | }); 256 | }); 257 | } 258 | 259 | blog.removeAll = function(callback) { 260 | if (!open) return queue.push(function() { 261 | blog.removeAll(callback); 262 | }); 263 | 264 | redis.removeAllPosts(function(err) { 265 | if (err) return callback(err); 266 | 267 | fsh.removeAllPosts(function(err) { 268 | if (err) return callback(err); 269 | callback(null); 270 | }); 271 | }); 272 | } 273 | 274 | //Deprecated methods 275 | blog.index = function(callback) { 276 | console.log('index is deprecated and will be removed in the next version.'); 277 | } 278 | 279 | blog.refresh = function() { 280 | console.log('refresh is deprecated and will be removed in the next version.'); 281 | } 282 | 283 | //The module itself is an event-based object. 284 | module.exports = blog; 285 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | reed 2 | ==== 3 | 4 | A Markdown-based blogging and website core backed by Redis and the 5 | filesystem. 6 | 7 | Features: 8 | 9 | * Asynchronously turn all markdown (.md) files in a directory into a blog 10 | stored in the hyper-fast Redis database. 11 | * Turn all markdown files in a (separate) directory into static pages. 12 | * Files are watched for changes and the Redis store is automagically updated. 13 | * Transparently access Redis or the filesystem to find a blog post. 14 | * Markdown metadata to describe your blog posts. 15 | * Fully event-based programming paradigm. 16 | 17 | What reed does not do: 18 | 19 | * Comments. Use a system like Disqus or roll your own. Comments might be added 20 | as a separate library later. 21 | 22 | What is reed? 23 | ------------- 24 | Reed is a (very) lightweight blogging **core** that turns markdown files in a 25 | directory into a blog. It is **not** a fully featured blogging system. If you 26 | are looking for that, check out Wheat or another blog engine. 27 | 28 | Reed is intended for developers who want to integrate simple blogging 29 | functionality into their node.js website or application. It makes as little 30 | assumptions as possible about your environment in order to give maximum 31 | flexibility. 32 | 33 | How to use reed 34 | ---------------- 35 | First, install it: 36 | 37 | `npm install reed` 38 | 39 | Make sure Redis 2.2 or greater is also installed and running. After 40 | Redis and reed are installed, use it thus: 41 | 42 | ```js 43 | var reed = require("reed"); 44 | reed.on("ready", function() { 45 | //ready. 46 | reed.get("a post", function(err, metadata, html) { 47 | //you have a post. 48 | }); 49 | }); 50 | 51 | reed.open("."); //looks for .md files in current directory. 52 | ``` 53 | 54 | In the above example, .md files will be pulled out of the current directory and 55 | indexed into the Redis database by the `index` function. After having indexed 56 | them, we can `list` the titles in order of post/updated date (that is, last 57 | modified date). 58 | 59 | Configuration 60 | ------------- 61 | Reed can connect to Redis running on separate hosts, non-standard 62 | ports or using authentication. This requires the use of the 63 | `configure` function before calling `open`. 64 | 65 | ```js 66 | var reed = require("reed"); 67 | 68 | //configure reed to connect to another redis 69 | //must be done *before* reed.open() 70 | reed.configure({ 71 | host: 'some.other.host.org', 72 | port: 1337, 73 | password: '15qe93rktkf39i4' 74 | }); 75 | 76 | reed.on("ready", function() { 77 | //ready. 78 | reed.get("a post", function(err, metadata, html) { 79 | //you have a post. 80 | }); 81 | }); 82 | 83 | reed.open("."); //looks for .md files in current directory. 84 | ``` 85 | 86 | Any property not overridden in the configuration object will use the 87 | Redis defaults. For example, it is possible to override just the port. 88 | 89 | Retrieving Posts 90 | ---------------- 91 | To retrieve an individual post and its associated metadata, use the `get` 92 | function: 93 | 94 | ```js 95 | reed.get("First Post", function(err, metadata, htmlContent) { 96 | console.log(JSON.stringify(metadata); 97 | console.log(htmlContent); 98 | }); 99 | ``` 100 | 101 | If retrieval of the post was successful, `err` will be null. `metadata` will be 102 | an object containing a `markdown` property that stores the original markdown 103 | text, a `lastModified` property that stores the last modified date as UNIX 104 | epoch time, plus any user-defined information (see below). `htmlContent` will be 105 | the post content, converted from markdown to HTML. 106 | 107 | If the post could not be retrieved, `err` will be an object containing error 108 | information (exactly what depends on the error thrown), and other two objects 109 | will be `undefined`. 110 | 111 | Note that the `get` function will hit the Redis database first, and then look 112 | on the filesystem for a title. So, if you have a new post that has not yet 113 | been indexed, it will get automagically added to the index via `get`. 114 | 115 | ### Article Naming and Metadata ### 116 | Every article in the blog is a markdown file in the specified directory. The 117 | filename is considered the "id" or "slug" of the article, and must be named 118 | accordingly. Reed article ids must have no spaces. Instead, spaces are mapped 119 | from `-`s: 120 | 121 | > "the first post" -> the-first-post.md 122 | 123 | These ids are case sensitive, so The-First-Post.md is different than 124 | the-first-post.md. 125 | 126 | #### Metadata #### 127 | Similar to Wheat, articles support user-defined metadata at the top of the 128 | article. These take the form of simple headers. They are transferred into the 129 | metadata object as properties. 130 | 131 | the-first-post.md: 132 | 133 | ``` 134 | Title: The First Post 135 | Author: me 136 | SomeOtherField: 123skidoo 137 | ``` 138 | 139 | The headers will be accessible thus: 140 | 141 | * metadata.title 142 | * metadata.author 143 | * metadata.someOtherField 144 | 145 | Field names can only alphabetical characters. So, "Some-Other-Field" is not a 146 | valid article header. 147 | 148 | Note: starting in 0.9.3, metadata fields are camelCase, rather than all lower 149 | case. 150 | 151 | Blog API 152 | -------- 153 | Reed exposes the following functions: 154 | 155 | * `configure(options)`: Configures reed. The options object can be 156 | used to specify connection settings for Redis. Supported settings 157 | are `host`, `port` and `password`. Any such configuration must be done 158 | *before* calling `open`. 159 | * `open(dir)`: Opens the given path for reed. When first opened, reed will scan 160 | the directory for .md files and add them to redis. 161 | * `close()`: Closes reed, shuts down the Redis connection, stops watching all 162 | .md files, and clears up state. 163 | * `get(id, callback)`: Retrieves a blog post. The callback receives `error`, 164 | `metadata`, and `htmlContent`. 165 | * `all(callback)`: Retrieves all blog posts. The callback receives `error` and 166 | `posts`, which is a list of post objects, each containing `metadata` and 167 | `htmlContent` properties. 168 | * `getMetadata(id, callback)`: Retrieves only the metadata for a blog post. The 169 | callback receives `error` (if there was an error), and `metadata`, an object 170 | containing the metadata from the blog post. 171 | * `list(callback)`: Retrieves all post IDs, sorted by last modified date. The 172 | callback receives `error` if there was an error, and `titles`, which is a 173 | list of post IDs. 174 | * `remove(id, callback)`: Removes a blog post from reed and the filesystem. 175 | The callback receives `error`, if an error occurred. 176 | * `removeAll(callback)`: Removes all blog posts from reed and the filesystem. 177 | The callback is called after all posts have been deleted, and receives `error` 178 | if there was an error during deletion. 179 | 180 | **Note**: `get`, `list`, `index`, `remove`, and `removeAll` asynchronously 181 | block until reed is in a ready state. This means they can be called before 182 | `open`, and they will run after opening has completed. 183 | 184 | Reed exposes the following events: 185 | 186 | * `error`: Fired when there is an error in certain internal procedures. Usually, 187 | inspecting the error object of callbacks is sufficient. 188 | * `ready`: Fired when reed has loaded. 189 | * `add`: Fired when a post is added to the blog. Note: posts updated while reed 190 | is not running are currently considered `add` events. 191 | * `update`: Fired when a blog post is updated while reed is running. Note: posts 192 | updated while reed is not running are currently considered `add` events. 193 | * `remove`: Fired when a blog post is removed (from the filesystem, through an 194 | API call, etc). The callback receives the full path of the file that was 195 | removed. 196 | 197 | Pages API 198 | --------- 199 | Reed 0.9 introduces pages functionality. This operates similarly to the blog 200 | functionality. Each page is a markdown file in a specified directory. The main 201 | difference is that the pages API is not indexed like blog posts are. There 202 | are no events exposed by the pages API, and there is no way to get a list of 203 | all pages in the system. 204 | 205 | This functionality is useful for static pages on a website. A simple example, 206 | using [Express](http://www.expressjs.com) to send the HTML of a reed page to 207 | a user: 208 | 209 | ```javascript 210 | app.get('/pages/:page', function(req, res) { 211 | reed.pages.get(req.params.page, function(err, metadata, htmlContent) { 212 | //In a real scenario, you should use a view 213 | //and make use of the metadata object. 214 | if (err) { 215 | res.send('There was an error: ' + JSON.stringify(err)); 216 | } 217 | else { 218 | res.send(htmlContent); 219 | } 220 | }); 221 | }); 222 | ``` 223 | 224 | The pages API is contained within the `pages` namespace: 225 | 226 | * `pages.open(dir, callback)`: Opens the given path for reed pages. This 227 | directory should be separate from the blog directory. Calling open() 228 | more than once will cause it to throw an error. The callback is called once 229 | the page system is running. 230 | * `pages.get(title, callback)`: Attempts to find the page with the given title. 231 | The callback receives `error`, `metadata`, and `htmlContent`, as in the 232 | regular `get` method. 233 | * `pages.remove(title, callback)`: Removes the specified page, deleting it from 234 | Redis and the filesystem. 235 | * `pages.close()`: Closes the pages portion of reed. 236 | 237 | Reed pages exposes the following events: 238 | 239 | * `error`: Fired when there is an error in certain internal procedures. Usually, 240 | inspecting the error object of callbacks is sufficient. 241 | * `ready`: Fired when reed has loaded. 242 | * `add`: Fired when a page is added. 243 | * `update`: Fired when a page is updated. 244 | * `remove`: Fired when a page is removed (from the filesystem, through an 245 | API call, etc). The callback receives the full path of the file that was 246 | removed. 247 | 248 | 249 | Contributors 250 | ============ 251 | These people have contributed to the development of reed in some way or another: 252 | 253 | * [ProjectMoon](https://github.com/ProjectMoon): primary author. 254 | * [algesten](https://github.com/algesten): bug fixes and redis conf. 255 | 256 | License 257 | ======= 258 | MIT License. Detailed in the LICENSE file. 259 | --------------------------------------------------------------------------------