@@ -15,9 +15,7 @@ Journo is a blogging program, with a few basic goals. To wit:
15
15
16
16
* Retina ready.
17
17
18
- * Syntax highlight code and handle large photo slideshows.
19
-
20
- * Handle minimal metadata (photos have captions, posts have titles).
18
+ * Syntax highlight code.
21
19
22
20
* Publish a feed.
23
21
@@ -33,57 +31,70 @@ Journo is a blogging program, with a few basic goals. To wit:
33
31
Write in Markdown
34
32
-----------------
35
33
36
- We'll use the excellent `marked` module to compile Markdown into HTML, and
37
- Underscore for many of its goodies later on.
34
+ We'll use the excellent **marked** module to compile Markdown into HTML, and
35
+ Underscore for many of its goodies later on. Up top, create a namespace for
36
+ shared values needed by more than one function.
38
37
39
38
marked = require ' marked'
40
39
_ = require ' underscore'
41
40
shared = {}
42
41
43
- A Journo site has a layout file, stored in `layout.html` . To render a post, we
44
- take its raw **source** , treat it as both an Underscore template (for HTML
45
- generation) and as Markdown (for formatting), and insert it into the layout
46
- as `content` .
42
+ To render a post, we take its raw `source` , treat it as both an Underscore
43
+ template (for HTML generation) and as Markdown (for formatting), and insert it
44
+ into the layout as `content` .
47
45
48
46
Journo .render = (post , source ) ->
49
47
catchErrors ->
48
+ do loadLayout
50
49
source or= fs .readFileSync postPath post
51
- shared .layout or= _ .template (fs .readFileSync (' layout.html' ).toString ())
52
50
variables = renderVariables post
53
51
markdown = _ .template (source .toString ()) variables
54
- title = postTitle markdown
52
+ title = detectTitle markdown
55
53
content = marked .parser marked .lexer markdown
56
54
shared .layout _ .extend variables, {title, content}
57
55
56
+ A Journo site has a layout file, stored in `layout.html` , which is used
57
+ to wrap every page.
58
+
59
+ loadLayout = (force ) ->
60
+ return layout if not force and layout = shared .layout
61
+ shared .layout = _ .template (fs .readFileSync (' layout.html' ).toString ())
62
+
58
63
59
64
Publish to Flat Files
60
65
---------------------
61
66
62
67
A blog is a folder on your hard drive. Within the blog, you have a `posts`
63
68
folder for blog posts, a `public` folder for static content, a `layout.html`
64
- file, and a `journo.json` file for configuration. During a `build` , a static
65
- version of the site is rendered into the `site` folder.
69
+ file for the layout which wraps every page, and a `journo.json` file for
70
+ configuration. During a `build` , a static version of the site is rendered
71
+ into the `site` folder, by **rsync** ing over all static files, rendering and
72
+ writing every post, and creating an RSS feed.
66
73
67
74
fs = require ' fs'
68
75
path = require ' path'
69
76
{spawn , exec } = require ' child_process'
70
77
71
78
Journo .build = ->
72
- loadConfig ()
73
- loadManifest ()
79
+ do loadManifest
74
80
fs .mkdirSync (' site' ) unless fs .existsSync (' site' )
75
- exec " rsync -vur --delete public site" , (err , stdout , stderr ) ->
81
+
82
+ exec " rsync -vur --delete public/ site" , (err , stdout , stderr ) ->
76
83
throw err if err
84
+
77
85
for post in folderContents (' posts' )
78
86
html = Journo .render post
79
87
file = htmlPath post
80
88
fs .mkdirSync path .dirname (file) unless fs .existsSync path .dirname (file)
81
89
fs .writeFileSync file, html
90
+
82
91
fs .writeFileSync " site/feed.rss" , Journo .feed ()
83
92
84
- The `config.json` configuration file is where you keep the nitty gritty details,
85
- like how to connect to your FTP server. The settings are: `host` , `port` ,
86
- `secure` , `user` , and `password` .
93
+ The `config.json` configuration file is where you keep the configuration
94
+ details of your blog, and how to connect to the server you'd like to publish
95
+ it on. The valid settings are: `title` , `description` , `author` (for RSS), `url
96
+ `, ` publish` (the ` user@host:path` location to **rsync** to), and ` publishPort`
97
+ (if your server doesn't listen to SSH on the usual one).
87
98
88
99
loadConfig = ->
89
100
return if shared .config
@@ -97,10 +108,13 @@ like how to connect to your FTP server. The settings are: `host`, `port`,
97
108
Publish via rsync
98
109
-----------------
99
110
111
+ Publishing is nice and rudimentary. We build out an entirely static version of
112
+ the site and **rysnc** it up to the server.
113
+
100
114
Journo .publish = ->
101
- Journo .build ()
115
+ do Journo .build
102
116
port = " ssh -p #{ shared .config .publishPort or 22 } "
103
- rsync = spawn " rsync" , [' -vurz' , ' --delete' , ' -e' , port, ' site' , shared .config .publish ]
117
+ rsync = spawn " rsync" , [' -vurz' , ' --delete' , ' -e' , port, ' site/ ' , shared .config .publish ]
104
118
rsync .stdout .on ' data' , (out ) -> console .log out .toString ()
105
119
rsync .stderr .on ' data' , (err ) -> console .error err .toString ()
106
120
@@ -114,69 +128,74 @@ and last recorded modified time) about each post.
114
128
manifestPath = ' journo-manifest.json'
115
129
116
130
loadManifest = ->
131
+ do loadConfig
132
+
117
133
shared .manifest = if fs .existsSync manifestPath
118
134
JSON .parse fs .readFileSync manifestPath
119
135
else
120
136
{}
121
- todo = compareManifest ()
122
- writeManifest ()
123
- todo
124
137
125
- writeManifest = ->
138
+ do updateManifest
126
139
fs .writeFileSync manifestPath, JSON .stringify shared .manifest
127
140
128
141
We update the manifest by looping through every post and every entry in the
129
- existing manifest, and looking for differences. Return a list of the posts
130
- that need to be `PUT` to the server, and the posts that should be `DELETE` d .
142
+ existing manifest, looking for differences in `mtime` , and recording those
143
+ along with the title and description of each post .
131
144
132
- compareManifest = ->
145
+ updateManifest = ->
146
+ manifest = shared .manifest
133
147
posts = folderContents ' posts'
134
- puts = []
135
- deletes = []
136
- for file, meta of shared .manifest when file not in posts
137
- deletes .push file
138
- delete shared .manifest [file]
139
- for file in posts
140
- stat = fs .statSync " posts/#{ file} "
141
- entry = shared .manifest [file]
148
+
149
+ delete manifest[post] for post of manifest when post not in posts
150
+
151
+ for post in posts
152
+ stat = fs .statSync postPath post
153
+ entry = manifest[post]
142
154
if not entry or entry .mtime isnt stat .mtime
143
- entry or= {pubtime : new Date }
155
+ entry or= {pubtime : stat . ctime }
144
156
entry .mtime = stat .mtime
145
- content = fs .readFileSync (" posts/#{ file} " ).toString ()
146
- entry .title = postTitle content
147
- puts .push file
148
- shared .manifest [file] = entry
149
- {puts, deletes}
157
+ content = fs .readFileSync (postPath post).toString ()
158
+ entry .title = detectTitle content
159
+ entry .description = detectDescription content, post
160
+ manifest[post] = entry
161
+
162
+ yes
150
163
151
164
152
165
Retina Ready
153
166
------------
154
167
168
+ In the future, it may make sense for Journo to have some sort of built-in
169
+ facility for automatically downsizing photos from retina to regular sizes ...
170
+ But for now, this bit is up to you.
171
+
155
172
156
173
Syntax Highlight Code
157
174
---------------------
158
175
176
+ We syntax-highlight blocks of code with the nifty **highlight** package that
177
+ includes heuristics for auto-language detection, so you don't have to specify
178
+ what you're coding in.
179
+
159
180
{Highlight } = require ' highlight'
160
181
161
182
marked .setOptions
162
183
highlight : (code , lang ) ->
163
184
Highlight code
164
185
165
186
166
- Create Photo Slideshows
167
- -----------------------
168
-
169
-
170
- Handle Minimal Metadata
171
- -----------------------
172
-
173
-
174
187
Publish a Feed
175
188
--------------
176
189
190
+ We'll use the **rss** module to build a simple feed of recent posts. Start with
191
+ the basic `author` , blog `title` , `description` and `url` configured in the
192
+ `config.json` . Then, each post's `title` is the first header present in the
193
+ post, the `description` is the first paragraph, and the date is the date you
194
+ first created the post file.
195
+
177
196
Journo .feed = ->
178
197
RSS = require ' rss'
179
- loadConfig ()
198
+ do loadConfig
180
199
config = shared .config
181
200
182
201
feed = new RSS
@@ -187,28 +206,26 @@ Publish a Feed
187
206
author : config .author
188
207
189
208
for post in sortedPosts ()[0 ... 20 ]
190
- content = fs .readFileSync (postPath post).toString ()
191
- lexed = marked .lexer content
192
- title = postTitle content
193
- description = _ .find (lexed, (token ) -> token .type is ' paragraph' )? .text + ' ...'
194
- description = marked .parser marked .lexer _ .template (description)(renderVariables (post))
195
-
209
+ entry = shared .manifest [post]
196
210
feed .item
197
- title : title
198
- description : description
211
+ title : entry . title
212
+ description : entry . description
199
213
url : postUrl post
200
- date : shared . manifest [post] .pubtime
214
+ date : entry .pubtime
201
215
202
216
feed .xml ()
203
217
204
218
205
219
Quickly Bootstrap a New Blog
206
220
----------------------------
207
221
222
+ We **init** a new blog into the current directory by copying over the contents
223
+ of a basic `bootstrap` folder.
224
+
208
225
Journo .init = ->
209
226
here = fs .realpathSync ' .'
210
227
if fs .existsSync ' posts'
211
- return console . error " A blog already exists in #{ here} "
228
+ fatal " A blog already exists in #{ here} "
212
229
bootstrap = path .join (__dirname , ' bootstrap' )
213
230
exec " rsync -vur --delete #{ bootstrap} ." , (err , stdout , stderr ) ->
214
231
throw err if err
@@ -218,37 +235,56 @@ Quickly Bootstrap a New Blog
218
235
Preview via a Local Server
219
236
--------------------------
220
237
238
+ Instead of constantly rebuilding a purely static version of the site, Journo
239
+ provides a preview server (which you can start by just typing `journo` from
240
+ within your blog).
241
+
221
242
Journo .preview = ->
222
243
http = require ' http'
223
244
mime = require ' mime'
224
245
url = require ' url'
225
246
util = require ' util'
226
- loadConfig ()
227
- loadManifest ()
247
+ do loadManifest
248
+
228
249
server = http .createServer (req, res) ->
229
250
rawPath = url .parse (req .url ).pathname .replace (/ (^ \/ | \/ $ )/ g , ' ' ) or ' index'
251
+
252
+ If the request is for a preview of the RSS feed...
253
+
230
254
if rawPath is ' feed.rss'
231
255
res .writeHead 200 , ' Content-Type' : mime .lookup (' .rss' )
232
256
res .end Journo .feed ()
257
+
258
+ If the request is for a static file that exists in our `public` directory...
259
+
233
260
else
234
261
publicPath = " public/" + rawPath
235
262
fs .exists publicPath, (exists ) ->
236
263
if exists
237
264
res .writeHead 200 , ' Content-Type' : mime .lookup (publicPath)
238
265
fs .createReadStream (publicPath).pipe res
266
+
267
+ If the request is for the slug of a valid post, we reload the layout, and
268
+ render it...
269
+
239
270
else
240
271
post = " posts/#{ rawPath} .md"
241
272
fs .exists post, (exists ) ->
242
273
if exists
274
+ loadLayout true
243
275
fs .readFile post, (err , content ) ->
244
276
res .writeHead 200 , ' Content-Type' : ' text/html'
245
277
res .end Journo .render post, content
278
+
279
+ Anything else is a 404.
280
+
246
281
else
247
282
res .writeHead 404
248
283
res .end ' 404 Not Found'
249
284
250
285
server .listen 1234
251
286
console .log " Journo is previewing at http://localhost:1234"
287
+ exec " open http://localhost:1234"
252
288
253
289
254
290
Work Without JavaScript, But Default to a Fluid JavaScript-Enabled UI
@@ -267,50 +303,85 @@ as well as readers.
267
303
Finally, Putting it all Together. Run Journo From the Terminal
268
304
--------------------------------------------------------------
269
305
306
+ We'll do the simplest possible command-line interface. If a public function
307
+ exists on the `Journo` object, you can run it. *Note that this lets you do
308
+ silly things, like* `journo toString` *but no big deal.*
309
+
270
310
Journo .run = ->
271
- args = process .argv .slice 2
272
- command = args[0 ] or ' preview'
273
- if Journo[command]
274
- do Journo[command]
275
- else
276
- console .error " Journo doesn't know how to '#{ command} '"
311
+ command = process .argv [2 ] or ' preview'
312
+ return do Journo[command] if Journo[command]
313
+ console .error " Journo doesn't know how to '#{ command} '"
277
314
278
315
279
316
Miscellaneous Bits and Utilities
280
317
--------------------------------
281
318
282
- For convenience, keep functions handy for finding the local file path to a post,
283
- and the URL for a post on the server.
319
+ Little utility functions that are useful up above.
320
+
321
+ The file path to the source of a given `post` .
284
322
285
323
postPath = (post ) -> " posts/#{ post} "
286
324
325
+ The server-side path to the HTML for a given `post` .
326
+
287
327
htmlPath = (post ) ->
288
328
name = postName post
289
329
if name is ' index'
290
330
' site/index.html'
291
331
else
292
332
" site/#{ name} /index.html"
293
333
334
+ The name (or slug) of a post, taken from the filename.
335
+
294
336
postName = (post ) -> path .basename post, ' .md'
295
337
338
+ The full, absolute URL for a published post.
339
+
296
340
postUrl = (post ) -> " #{ shared .siteUrl } /#{ postName (post)} /"
297
341
298
- postTitle = (content ) ->
342
+ Starting with the string contents of a post, detect the title --
343
+ the first heading.
344
+
345
+ detectTitle = (content ) ->
299
346
_ .find (marked .lexer (content), (token ) -> token .type is ' heading' )? .text
300
347
348
+ Starting with the string contents of a post, detect the description --
349
+ the first paragraph.
350
+
351
+ detectDescription = (content , post ) ->
352
+ desc = _ .find (marked .lexer (content), (token ) -> token .type is ' paragraph' )? .text
353
+ marked .parser marked .lexer _ .template (" #{ desc} ..." )(renderVariables (post))
354
+
355
+ Helper function to read in the contents of a folder, ignoring hidden files
356
+ and directories.
357
+
301
358
folderContents = (folder ) ->
302
359
fs .readdirSync (folder).filter (f) -> f .charAt (0 ) isnt ' .'
303
360
361
+ Return the list of posts currently in the manifest, sorted by their date of
362
+ publication.
363
+
304
364
sortedPosts = ->
305
365
_ .sortBy _ .without (_ .keys (shared .manifest ), ' index.md' ), (post ) ->
306
366
shared .manifest [post].pubtime
307
367
308
- The shared variables we want to allow our templates to use in their evaluations.
368
+ The shared variables we want to allow our templates (both posts, and layout)
369
+ to use in their evaluations.
309
370
310
371
renderVariables = (post ) ->
311
- {_, fs, path, folderContents, mapLink, postName, post : path .basename (post), posts : sortedPosts (), manifest : shared .manifest }
312
-
313
- Quick function to creating a link to a Google Map.
372
+ {
373
+ _
374
+ fs
375
+ path
376
+ mapLink
377
+ postName
378
+ folderContents
379
+ posts : sortedPosts ()
380
+ post : path .basename (post)
381
+ manifest : shared .manifest
382
+ }
383
+
384
+ Quick function which creates a link to a Google Map of the place.
314
385
315
386
mapLink = (place , additional = ' ' , zoom = 15 ) ->
316
387
query = encodeURIComponent (" #{ place} , #{ additional} " )
@@ -320,13 +391,12 @@ Convenience function for catching errors (keeping the preview server from
320
391
crashing while testing code), and printing them out.
321
392
322
393
catchErrors = (func ) ->
323
- try
324
- func ()
394
+ try do func
325
395
catch err
326
396
console .error err .stack
327
397
" <pre>#{ err .stack } </pre>"
328
398
329
- And then for errors that you want the app to die on -- things that would break
399
+ Finally, for errors that you want the app to die on -- things that should break
330
400
the site build.
331
401
332
402
fatal = (message ) ->
0 commit comments