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.
... let's go through these one at a time:
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.
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
.
= catchErrors -> do loadLayout source or= fsreadFileSync postPath post variables = renderVariables post markdown = _templatesourcetoString variables title = detectTitle markdown content = markedparser markedlexer markdown sharedlayout _extend variablestitlecontent
A Journo site has a layout file, stored in layout.html
, which is used
to wrap every page.
= return layout if not force and layout = sharedlayout shared.layout = _templatefsreadFileSync'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.
fs = require 'fs'path = require 'path' require 'child_process' = -> do loadManifest fsmkdirSync'site' unless fsexistsSync'site' exec "rsync -vur --delete public/ site" throw err if err for post in folderContents'posts' html = Journorender post file = htmlPath post fsmkdirSync pathdirnamefile unless fsexistsSync pathdirnamefile fswriteFileSync filehtml fswriteFileSync "site/feed.rss"Journofeed
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 user@host: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.
= -> return if sharedconfig try shared.config = JSONparse fsreadFileSync 'config.json' catch err fatal "Unable to read config.json" shared.siteUrl = sharedconfigurlreplace/\/$/''
Publish via rsync
Publishing is nice and rudimentary. We build out an entirely static version of the site and rysnc it up to the server.
= -> do Journobuild rsync 'site/images/'pathjoinsharedconfigpublish'images/'-> rsync 'site/'sharedconfigpublish
A helper function for rsyncing, 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.
= port = "ssh -p " child = spawn "rsync"'-vurz''--delete''-e'portfromto childstdouton 'data' consolelog outtoString childstderron 'data' consoleerror errtoString childon 'exit'callback if callback
Maintain a Manifest File
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.
manifestPath = 'journo-manifest.json' = -> do loadConfig shared.manifest = if fsexistsSync manifestPath JSONparse fsreadFileSync manifestPath else do updateManifest fswriteFileSync manifestPathJSONstringify sharedmanifest
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.
= -> manifest = sharedmanifest posts = folderContents 'posts' delete manifestpostfor post of manifest when post not in posts for post in posts stat = fsstatSync postPath post entry = manifestpost if not entry or entrymtime isnt statmtime entry or= pubtime: statctime entry.mtime = statmtime content = fsreadFileSyncpostPath posttoString entry.title = detectTitle content entry.description = detectDescription contentpost manifestpost= entry yes
Retina Ready
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.
Syntax Highlight Code
We syntax-highlight blocks of code with the nifty highlight package that includes heuristics for auto-language detection, so you don't have to specify what you're coding in.
require 'highlight' markedsetOptions : Highlight code
Publish a Feed
We'll use the rss module to build a simple feed of recent posts. Start with
the basic author
, blog title
, description
and url
configured in the
config.json
. Then, each post's title
is the first header present in the
post, the description
is the first paragraph, and the date is the date you
first created the post file.
= -> RSS = require 'rss' do loadConfig config = sharedconfig feed = title: configtitle description: configdescription feed_url: "/rss.xml" site_url: sharedsiteUrl author: configauthor for post in sortedPosts0...20 entry = sharedmanifestpost feeditem title: entrytitle description: entrydescription url: postUrl post date: entrypubtime feedxml
Quickly Bootstrap a New Blog
We init a new blog into the current directory by copying over the contents
of a basic bootstrap
folder.
= -> here = fsrealpathSync '.' if fsexistsSync 'posts' fatal "A blog already exists in " bootstrap = pathjoin__dirname'bootstrap' exec "rsync -vur --delete ." throw err if err consolelog "Initialized new blog in "
Preview via a Local Server
Instead of constantly rebuilding a purely static version of the site, Journo
provides a preview server (which you can start by just typing journo
from
within your blog).
= -> http = require 'http' mime = require 'mime' url = require 'url' util = require 'util' do loadManifest server = httpcreateServer rawPath = urlparserequrlpathnamereplace//g'' or 'index'
If the request is for a preview of the RSS feed...
if rawPath is 'feed.rss' reswriteHead 200'Content-Type': mimelookup'.rss' resend Journofeed
If the request is for a static file that exists in our public
directory...
else publicPath = "public/" + rawPath fsexists publicPath if exists reswriteHead 200'Content-Type': mimelookuppublicPath fscreateReadStreampublicPathpipe res
If the request is for the slug of a valid post, we reload the layout, and render it...
else post = "posts/.md" fsexists post if exists loadLayout true fsreadFile post reswriteHead 200'Content-Type': 'text/html' resend Journorender postcontent
Anything else is a 404. (Does anyone know a cross-platform equivalent of the
OSX open
command?)
else reswriteHead 404 resend '404 Not Found' serverlisten 1234 consolelog "Journo is previewing at http://localhost:1234" exec "open http://localhost:1234"
Work Without JavaScript, But Default to a Fluid JavaScript-Enabled UI
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 alt
attributes for captions, for example.
Since the blog is public, it's nice if search engines can see all of the pieces as well as readers.
Finally, Putting it all Together. Run Journo From the Terminal
We'll do the simplest possible command-line interface. If a public function
exists on the Journo
object, you can run it. Note that this lets you do
silly things, like journo toString
but no big deal.
= -> command = processargv2or 'preview' return do Journocommandif Journocommand consoleerror "Journo doesn't know how to ''"
Let's also provide a help page that lists the available commands.
Journo.help = Journo'--help'= -> consolelog """ Usage: journo [command] 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 """
And we might as well do the version number, for completeness' sake.
Journo.version = Journo'--version'= -> consolelog "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
.
= "posts/"
The server-side path to the HTML for a given post
.
= name = postName post if name is 'index' 'site/index.html' else "site//index.html"
The name (or slug) of a post, taken from the filename.
= pathbasename post'.md'
The full, absolute URL for a published post.
= "//"
Starting with the string contents of a post, detect the title -- the first heading.
= _findmarkedlexercontent tokentype is 'heading'?text
Starting with the string contents of a post, detect the description -- the first paragraph.
= desc = _findmarkedlexercontent tokentype is 'paragraph'?text markedparser markedlexer _template"..."renderVariablespost
Helper function to read in the contents of a folder, ignoring hidden files and directories.
= fsreaddirSyncfolderfilter fcharAt0 isnt '.'
Return the list of posts currently in the manifest, sorted by their date of publication.
= -> _sortBy _without_keyssharedmanifest'index.md' sharedmanifestpostpubtime
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.
= _ fs path mapLink postName folderContents posts: sortedPosts post: pathbasenamepost manifest: sharedmanifest
Quick function which creates a link to a Google Map search for the name of the place.
= query = encodeURIComponent", " "<a href=\"https://maps.google.com/maps?q=&t=h&z=\"></a>"
Convenience function for catching errors (keeping the preview server from crashing while testing code), and printing them out.
= try do func catch err consoleerror errstack "<pre></pre>"
Finally, for errors that you want the app to die on -- things that should break the site build.
= consoleerror message processexit 1