Need help with journo?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.
jashkenas

Description

A quick-and-dirty (literate) blogging engine

424 Stars 88 Forks Other 40 Commits 6 Opened issues

Services available

Need anything else?

Journo

Journo = module.exports = {}

Journo is a blogging program, with a few basic goals. To wit:

  • Write in Markdown.

  • Publish to flat files.

  • Publish via Rsync.

  • Maintain a manifest file (what's published and what isn't, pub dates).

  • Retina ready.

  • Syntax highlight code.

  • Publish a feed.

  • Quickly bootstrap a new blog.

  • Preview via a local server.

  • Work without JavaScript, but default to a fluid JavaScript-enabled UI.

You can install and use the

journo
command via npm:
sudo npm install -g journo

... now, let's go through those features one at a time:

Getting Started

  1. Create a folder for your blog, and

    cd
    into it.
  2. Type

    journo init
    to bootstrap a new empty blog.
  3. Edit the

    config.json
    ,
    layout.html
    , and
    posts/index.md
    files to suit.
  4. Type

    journo
    to start the preview server, and have at it.

Write in Markdown

We'll use the excellent marked module to compile Markdown into HTML, and Underscore for many of its goodies later on. Up top, create a namespace for shared values needed by more than one function.

coffeescript
marked = require 'marked'
_ = require 'underscore'
shared = {}
To render a post, we take its raw
source
, treat it as both an Underscore template (for HTML generation) and as Markdown (for formatting), and insert it into the layout as
content
.
coffeescript
Journo.render = (post, source) ->
  catchErrors ->
    do loadLayout
    source or= fs.readFileSync postPath post
    variables = renderVariables post
    markdown  = _.template(source.toString()) variables
    title     = detectTitle markdown
    content   = marked.parser marked.lexer markdown
    shared.layout _.extend variables, {title, content}
A Journo site has a layout file, stored in
layout.html
, which is used to wrap every page.
coffeescript
loadLayout = (force) ->
  return layout if not force and layout = shared.layout
  shared.layout = _.template(fs.readFileSync('layout.html').toString())

Publish to Flat Files

A blog is a folder on your hard drive. Within the blog, you have a

posts
folder for blog posts, a
public
folder for static content, a
layout.html
file for the layout which wraps every page, and a
journo.json
file for configuration. During a
build
, a static version of the site is rendered into the
site
folder, by rsyncing over all static files, rendering and writing every post, and creating an RSS feed. ```coffeescript fs = require 'fs' path = require 'path' {spawn, exec} = require 'child_process'

Journo.build = -> do loadManifest fs.mkdirSync('site') unless fs.existsSync('site')

exec "rsync -vur --delete public/ site", (err, stdout, stderr) -> throw err if err

for post in folderContents('posts')
  html = Journo.render post
  file = htmlPath post
  fs.mkdirSync path.dirname(file) unless fs.existsSync path.dirname(file)
  fs.writeFileSync file, html

fs.writeFileSync "site/feed.rss", Journo.feed()

The config.json configuration file is where you keep the configuration
details of your blog, and how to connect to the server you'd like to publish
it on. The valid settings are: title, description, author (for RSS), url , publish (the [email protected]:path location to rsync to), and publishPort
(if your server doesn't listen to SSH on the usual one).

An example config.json will be bootstrapped for you when you initialize a blog, so you don't need to remember any of that.

loadConfig = ->
  return if shared.config
  try
    shared.config = JSON.parse fs.readFileSync 'config.json'
  catch err
    fatal "Unable to read config.json"
  shared.siteUrl = shared.config.url.replace(/\/$/, '')
</pre>
<h2>Publish via rsync</h2>

<p>Publishing is nice and rudimentary. We build out an entirely static version of
the site and <strong>rsync</strong> it up to the server.
</p><pre>coffeescript
Journo.publish = -&gt;
  do Journo.build
  rsync 'site/images/', path.join(shared.config.publish, 'images/'), -&gt;
    rsync 'site/', shared.config.publish
</pre>
A helper function for <strong>rsync</strong>ing, with logging, and the ability to wait for
the rsync to continue before proceeding. This is useful for ensuring that our
any new photos have finished uploading (very slowly) before the update to the feed
is syndicated out.
<pre>coffeescript
rsync = (from, to, callback) -&gt;
  port = "ssh -p #{shared.config.publishPort or 22}"
  child = spawn "rsync", ['-vurz', '--delete', '-e', port, from, to]
  child.stdout.on 'data', (out) -&gt; console.log out.toString()
  child.stderr.on 'data', (err) -&gt; console.error err.toString()
  child.on 'exit', callback if callback
</pre>

<h2>Maintain a Manifest File</h2>

<p>The "manifest" is where Journo keeps track of metadata -- the title, description,
publications date and last modified time of each post. Everything you need to
render out an RSS feed ... and everything you need to know if a post has been
updated or removed.
```coffeescript
manifestPath = 'journo-manifest.json'</p>

<p>loadManifest = -&gt;
  do loadConfig</p>

<p>shared.manifest = if fs.existsSync manifestPath
    JSON.parse fs.readFileSync manifestPath
  else
    {}</p>

<p>do updateManifest
  fs.writeFileSync manifestPath, JSON.stringify shared.manifest
</p><pre>
We update the manifest by looping through every post and every entry in the
existing manifest, looking for differences in `mtime`, and recording those
along with the title and description of each post.
</pre>coffeescript
updateManifest = -&gt;
  manifest = shared.manifest
  posts = folderContents 'posts'

<p>delete manifest[post] for post of manifest when post not in posts</p>

<p>for post in posts
    stat = fs.statSync postPath post
    entry = manifest[post]
    if not entry or entry.mtime isnt stat.mtime
      entry or= {pubtime: stat.ctime}
      entry.mtime = stat.mtime
      content = fs.readFileSync(postPath post).toString()
      entry.title = detectTitle content
      entry.description = detectDescription content, post
      manifest[post] = entry</p>

<p>yes
```</p>

<h2>Retina Ready</h2>

<p>In the future, it may make sense for Journo to have some sort of built-in
facility for automatically downsizing photos from retina to regular sizes ...
But for now, this bit is up to you.</p>

<h2>Syntax Highlight Code</h2>

<p>We syntax-highlight blocks of code with the nifty <strong>highlight</strong> package that
includes heuristics for auto-language detection, so you don't have to specify
what you're coding in.
```coffeescript
{Highlight} = require 'highlight'</p>

<p>marked.setOptions
  highlight: (code, lang) -&gt;
    Highlight code
```</p>

<h2>Publish a Feed</h2>

<p>We'll use the <strong>rss</strong> module to build a simple feed of recent posts. Start with
the basic </p><pre>author</pre>, blog <pre>title</pre>, <pre>description</pre> and <pre>url</pre> configured in the
<pre>config.json</pre>. Then, each post's <pre>title</pre> is the first header present in the
post, the <pre>description</pre> is the first paragraph, and the date is the date you
first created the post file.
```coffeescript
Journo.feed = -&gt;
  RSS = require 'rss'
  do loadConfig
  config = shared.config

<p>feed = new RSS
    title: config.title
    description: config.description
    feed<em>url: "#{shared.siteUrl}/rss.xml"
    site</em>url: shared.siteUrl
    author: config.author</p>

<p>for post in sortedPosts()[0...20]
    entry = shared.manifest[post]
    feed.item
      title: entry.title
      description: entry.description
      url: postUrl post
      date: entry.pubtime</p>

<p>feed.xml()
```</p>

<h2>Quickly Bootstrap a New Blog</h2>

<p>We <strong>init</strong> a new blog into the current directory by copying over the contents
of a basic </p><pre>bootstrap</pre> folder.
<pre>coffeescript
Journo.init = -&gt;
  here = fs.realpathSync '.'
  if fs.existsSync 'posts'
    fatal "A blog already exists in #{here}"
  bootstrap = path.join(__dirname, 'bootstrap')
  exec "rsync -vur --delete #{bootstrap} .", (err, stdout, stderr) -&gt;
    throw err if err
    console.log "Initialized new blog in #{here}"
</pre>

<h2>Preview via a Local Server</h2>

<p>Instead of constantly rebuilding a purely static version of the site, Journo
provides a preview server (which you can start by just typing </p><pre>journo</pre> from
within your blog).
```coffeescript
Journo.preview = -&gt;
  http = require 'http'
  mime = require 'mime'
  url = require 'url'
  util = require 'util'
  do loadManifest

<p>server = http.createServer (req, res) -&gt;
    rawPath = url.parse(req.url).pathname.replace(/(^\/|\/$)/g, '') or 'index'
</p><pre>
If the request is for a preview of the RSS feed...
</pre>coffeescript
if rawPath is 'feed.rss'
      res.writeHead 200, 'Content-Type': mime.lookup('.rss')
      res.end Journo.feed()
<pre>
If the request is for a static file that exists in our `public` directory...
</pre>coffeescript
else
      publicPath = "public/" + rawPath
      fs.exists publicPath, (exists) -&gt;
        if exists
          res.writeHead 200, 'Content-Type': mime.lookup(publicPath)
          fs.createReadStream(publicPath).pipe res
<pre>
If the request is for the slug of a valid post, we reload the layout, and
render it...
</pre>coffeescript
    else
          post = "posts/#{rawPath}.md"
          fs.exists post, (exists) -&gt;
            if exists
              loadLayout true
              fs.readFile post, (err, content) -&gt;
                res.writeHead 200, 'Content-Type': 'text/html'
                res.end Journo.render post, content
<pre>
Anything else is a 404. (Does anyone know a cross-platform equivalent of the
OSX `open` command?)
</pre>coffeescript
        else
              res.writeHead 404
              res.end '404 Not Found'

<p>server.listen 1234
  console.log "Journo is previewing at http://localhost:1234"
  exec "open http://localhost:1234"
```</p>

<h2>Work Without JavaScript, But Default to a Fluid JavaScript-Enabled UI</h2>

<p>The best way to handle this bit seems to be entirely on the client-side. For
example, when rendering a JavaScript slideshow of photographs, instead of
having the server spit out the slideshow code, simply have the blog detect
the list of images during page load and move them into a slideshow right then
and there -- using </p><pre>alt</pre> attributes for captions, for example.

<p>Since the blog is public, it's nice if search engines can see all of the pieces
as well as readers.</p>

<h2>Finally, Putting it all Together. Run Journo From the Terminal</h2>

<p>We'll do the simplest possible command-line interface. If a public function
exists on the </p><pre>Journo</pre> object, you can run it. <em>Note that this lets you do
silly things, like</em> <pre>journo toString</pre> <em>but no big deal.</em>
<pre>coffeescript
Journo.run = -&gt;
  command = process.argv[2] or 'preview'
  return do Journo[command] if Journo[command]
  console.error "Journo doesn't know how to '#{command}'"
</pre>
Let's also provide a help page that lists the available commands.
```coffeescript
Journo.help = Journo['--help'] = -&gt;
  console.log """
    Usage: journo [command]
<pre>If called without a command, `journo` will preview your blog.

init      start a new blog in the current folder
build     build a static version of the blog into 'site'
preview   live preview the blog via a local server
publish   publish the blog to your remote server
</pre>
<p>"""
</p><pre>
And we might as well do the version number, for completeness' sake.
</pre>coffeescript
Journo.version = Journo['--version'] = -&gt;
  console.log "Journo 0.0.1"

Miscellaneous Bits and Utilities

Little utility functions that are useful up above.

The file path to the source of a given

post
.
coffeescript
postPath = (post) -> "posts/#{post}"
The server-side path to the HTML for a given
post
.
coffeescript
htmlPath = (post) ->
  name = postName post
  if name is 'index'
    'site/index.html'
  else
    "site/#{name}/index.html"
The name (or slug) of a post, taken from the filename.
coffeescript
postName = (post) -> path.basename post, '.md'
The full, absolute URL for a published post.
coffeescript
postUrl = (post) -> "#{shared.siteUrl}/#{postName(post)}/"
Starting with the string contents of a post, detect the title -- the first heading.
coffeescript
detectTitle = (content) ->
  _.find(marked.lexer(content), (token) -> token.type is 'heading')?.text
Starting with the string contents of a post, detect the description -- the first paragraph.
coffeescript
detectDescription = (content, post) ->
  desc = _.find(marked.lexer(content), (token) -> token.type is 'paragraph')?.text
  marked.parser marked.lexer _.template("#{desc}...")(renderVariables(post))
Helper function to read in the contents of a folder, ignoring hidden files and directories.
coffeescript
folderContents = (folder) ->
  fs.readdirSync(folder).filter (f) -> f.charAt(0) isnt '.'
Return the list of posts currently in the manifest, sorted by their date of publication.
coffeescript
sortedPosts = ->
  _.sortBy _.without(_.keys(shared.manifest), 'index.md'), (post) ->
    shared.manifest[post].pubtime
The shared variables we want to allow our templates (both posts, and layout) to use in their evaluations. In the future, it would be nice to determine exactly what best belongs here, and provide an easier way for the blog author to add functions to it.
coffeescript
renderVariables = (post) ->
  {
    _
    fs
    path
    mapLink
    postName
    folderContents
    posts: sortedPosts()
    post: path.basename(post)
    manifest: shared.manifest
  }
Quick function which creates a link to a Google Map search for the name of the place.
coffeescript
mapLink = (place, additional = '', zoom = 15) ->
  query = encodeURIComponent("#{place}, #{additional}")
  "#{place}"
Convenience function for catching errors (keeping the preview server from crashing while testing code), and printing them out.
coffeescript
catchErrors = (func) ->
  try do func
  catch err
    console.error err.stack
    "
#{err.stack}
"
Finally, for errors that you want the app to die on -- things that should break the site build.
coffeescript
fatal = (message) ->
  console.error message
  process.exit 1

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.