serif 0.0.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org/"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ serif (0.1.1)
5
+ liquid (~> 2.4)
6
+ pygments.rb (~> 0.3)
7
+ rack (~> 1.0)
8
+ rake (~> 0.9)
9
+ redcarpet (~> 2.2)
10
+ redhead (~> 0.0.6)
11
+ sinatra (~> 1.3)
12
+ slop (~> 3.3)
13
+ yui-compressor
14
+
15
+ GEM
16
+ remote: http://rubygems.org/
17
+ specs:
18
+ POpen4 (0.1.4)
19
+ Platform (>= 0.4.0)
20
+ open4
21
+ Platform (0.4.0)
22
+ liquid (2.4.1)
23
+ open4 (1.3.0)
24
+ posix-spawn (0.3.6)
25
+ pygments.rb (0.3.2)
26
+ posix-spawn (~> 0.3.6)
27
+ yajl-ruby (~> 1.1.0)
28
+ rack (1.4.1)
29
+ rack-protection (1.2.0)
30
+ rack
31
+ rake (0.9.2.2)
32
+ redcarpet (2.2.2)
33
+ redhead (0.0.6)
34
+ sinatra (1.3.3)
35
+ rack (~> 1.3, >= 1.3.6)
36
+ rack-protection (~> 1.2)
37
+ tilt (~> 1.3, >= 1.3.3)
38
+ slop (3.3.3)
39
+ tilt (1.3.3)
40
+ yajl-ruby (1.1.0)
41
+ yui-compressor (0.9.6)
42
+ POpen4 (>= 0.1.4)
43
+
44
+ PLATFORMS
45
+ ruby
46
+
47
+ DEPENDENCIES
48
+ serif!
data/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # Serif
2
+
3
+ Serif is a file-based blogging engine intended for simple sites. It compiles Markdown content to static files, and there is a web interface for editing and publishing ([simple video demo](https://docs.google.com/open?id=0BxPQpxGSOOyKS1J4MmlnM3JIaXM)), because managing everything with `ssh` and `git` can be a pain, compared to having a more universally accessible editing interface.
4
+
5
+ _It should be considered alpha._ I make no promises it won't all change underneath you. **It is all subject to change, and is in a rough state.**
6
+
7
+ # Intro
8
+
9
+ Serif is a lot like Jekyll with a few extra moving parts, although it didn't start that way. It went through two reworkings before being converted into something based on generated Markdown files. The aim for Serif is to provide two things:
10
+
11
+ 1. Simplicity: the source and generated content are just files that can be served by any web server.
12
+ 2. Ease of publishing, wherever you are: having everything based on files that you edit in a text editor is a nice idea, but what if you're on a machine that doesn't give you ssh access to your server? What if you need to edit creation timestamps? What about editing drafts without having to make commits and push to git repos?
13
+
14
+ With this in mind, you might think of Serif's aim as to merge Jekyll, [Second Crack](https://github.com/marcoarment/secondcrack) and ideas from [Svbtle](http://dcurt.is/codename-svbtle). There should be many ways of editing and publishing, such as using the web interface, `rsync`ing from a remote machine, or editing a draft file on the remote server and having everything happen for you. (This last feature doesn't quite exist as planned yet.)
15
+
16
+ ## Planned features
17
+
18
+ Some things I'm hoping to implement one day:
19
+
20
+ 1. Custom hooks to fire after particular events, such as minifying CSS after publish, or committing changes and pushing to a git repository.
21
+ 2. Simple Markdown pages instead of plain HTML.
22
+ 3. Automatically detecting file changes and regenerating the site.
23
+ 4. Adding custom Liquid filters and tags.
24
+
25
+ # License and contributing
26
+
27
+ Serif is released under the MIT license. See LICENSE for details.
28
+
29
+ Any contributions will be assumed by default to be under the same terms.
30
+
31
+ # Basics
32
+
33
+ ## Installing
34
+
35
+ Installation is via [RubyGems](https://rubygems.org/). If you don't have Ruby installed, I recommend using [RVM](https://rvm.io/).
36
+
37
+ ```bash
38
+ $ gem install serif
39
+ ```
40
+
41
+ ## Generating the site
42
+
43
+ ```bash
44
+ $ cd path/to/site/directory
45
+ $ serif generate
46
+ ```
47
+
48
+ (You may get warnings about "undefined method `gsub' for nil:NilClass" after a warning about trying to get headers out of a file. You should be able to ignore those.)
49
+
50
+ ## Starting the admin server
51
+
52
+ ```bash
53
+ $ cd path/to/site/directory
54
+ $ ENV=production serif admin
55
+ ```
56
+
57
+ Once this is run, visit <http://localhost:4567/admin> and log in with whatever is in `_config.yml` as auth credentials.
58
+
59
+ Drop the `ENV=production` part if you're running it locally.
60
+
61
+ ## Serving up the site for development
62
+
63
+ This runs a very simple web server that is mainly designed to test what the site will look like and let you make changes to stuff like CSS files without having to regenerate everything. Changes to post content will not be detected (yet).
64
+
65
+ ```bash
66
+ $ cd path/to/site/directory
67
+ $ serif dev
68
+ ```
69
+
70
+ Once this is run, visit <http://localhost:8000>.
71
+
72
+ ## Generate a skeleton application
73
+
74
+ You can generate a skeletal directory to get you going, using the `new` command.
75
+
76
+ ```bash
77
+ $ cd path/to/site/directory
78
+ $ serif new
79
+ ```
80
+
81
+ # Content and site structure
82
+
83
+ The structure of a Serif site is something like this:
84
+
85
+ ```
86
+ .
87
+ ├── _site
88
+ ├── _layouts
89
+ │   └── default.html
90
+ ├── _drafts
91
+ │   ├── some-draft
92
+ │   └── another-unfinished-post
93
+ ├── _posts
94
+ │   ├── 2012-01-01-a-post-you-have-written
95
+ │   ├── 2012-02-28-another-post
96
+ │   └── 2012-03-30-and-a-third
97
+ ├── _templates
98
+ │   └─── post.html
99
+ ├── _trash
100
+ ├── _config.yml
101
+ ├── css
102
+ │   └── ...
103
+ ├── js
104
+ │   └── ...
105
+ ├── images
106
+ │   └── ...
107
+ ├── 404.html
108
+ ├── favicon.ico
109
+ ├── feed.xml
110
+ └── index.html
111
+ ```
112
+
113
+ ## `_site`
114
+
115
+ This is where generated content gets saved. You should serve files out of here, be it with Nginx or Apache. You should assume everything in this directory will get erased at some point in future. Don't keep anything in it!
116
+
117
+ ## `_layouts`
118
+
119
+ This is where layouts for the site go. At the moment, there is only one supported: `default.html`.
120
+
121
+ ## `_drafts` and `_posts`
122
+
123
+ Drafts go in `_drafts`, posts go in `_posts`. Simple enough.
124
+
125
+ Posts must have filenames in the format of `YYYY-MM-DD-your-post`. Drafts do not have a date part, since they're drafts and not published.
126
+
127
+ All files in these directories are assumed to be written in Markdown, with simple HTTP-style headers. The Markdown renderer is [Redcarpet](https://github.com/vmg/redcarpet) (with fenced code blocks enabled), with Smarty for punctuation tweaks, and Pygments to allow syntax highlighting (although you'll need your own CSS).
128
+
129
+ Here's an example post:
130
+
131
+ ```
132
+ Title: A title of a post
133
+ Created: 2012-01-01T14:30:00+00:00
134
+
135
+ Something something.
136
+
137
+ 1. A list
138
+ 2. Of some stuff
139
+ 3. Goes here
140
+
141
+ End of the post
142
+ ```
143
+
144
+ The headers are similar to Jekyll's YAML front matter, but here there are no formatting requirements beyond `Key: value` pairs. Header names are case-insensitive (so `title` is the same as `Title`), but values are not.
145
+
146
+ (The headers `created` and `updated` must be a string that Ruby's standard Time library can parse, but this will mostly be handled for you.)
147
+
148
+ ## `_templates`
149
+
150
+ This directory currently only has a single file in it, `post.html`, which is used to produce HTML content from Markdown files in `_posts`.
151
+
152
+ ## `_trash`
153
+
154
+ Deleted drafts go in here just in case you want them back.
155
+
156
+ ## `_config.yml`
157
+
158
+ Used for configuration settings.
159
+
160
+ Here's a sample configuration:
161
+
162
+ ```yaml
163
+ admin:
164
+ username: username
165
+ password: password
166
+ permalink: /blog/:year/:month/:title
167
+ ```
168
+
169
+ If a permalink setting is not given in the configuration, the default is `/:title`. There are the following options available for permalinks:
170
+
171
+ Placeholder | Value
172
+ ----------- |:-----
173
+ `:title` | URL "slug", e.g., "your-post-title"
174
+ `:year` | Year as given in the filename, e.g., "2012"
175
+ `:month` | Month as given in the filename, e.g., "01"
176
+ `:day` | Day as given in the filename, e.g., "28"
177
+
178
+ ## Other files
179
+
180
+ Any other file in the directory's root will be copied over exactly as-is, with two caveats for any file ending in `.html` or `.xml`:
181
+
182
+ 1. These files are assumed to contain [Liquid markup](http://liquidmarkup.org/) and will be processed as such.
183
+ 2. Any header data will not be included in the processed output.
184
+
185
+ For example, this would work as an `about.html`:
186
+
187
+ ```html
188
+ <h1>All about me</h1>
189
+ <p>Where do I begin? {{ 'Well...' }}</p>
190
+ ```
191
+
192
+ And so would this:
193
+
194
+ ```html
195
+ title: My about page
196
+
197
+ <h1>All about me</h1>
198
+ <p>Where do I begin? Well...</p>
199
+ ```
200
+
201
+ In both cases, the output is, of course:
202
+
203
+ ```html
204
+ <h1>All about me</h1>
205
+ <p>Where do I begin? Well...</p>
206
+ ```
207
+
208
+ If you have a file like `feed.xml` that you wish to _not_ be contained within a layout, specify `layout: none` in the header for the file.
209
+
210
+ # Deploying
211
+
212
+ To serve the site, set any web server to use `/path/to/site/directory/_site` as its root. *NOTE:* URLs generated in the site do not contain `.html` "extensions" by default, so you will need a rewrite rule. Here's an example rewrite for nginx:
213
+
214
+ ```
215
+ error_page 404 @not_found_page;
216
+
217
+ location / {
218
+ index index.html index.htm;
219
+
220
+ try_files $uri.html $uri $uri/ @not_found_page;
221
+ }
222
+
223
+ location @not_found_page {
224
+ rewrite .* /404.html last;
225
+ }
226
+ ```
227
+
228
+ ## Admin interface
229
+
230
+ The admin server can be started on the live server the same way it's started locally (with `ENV=production`). To access it from anywhere on the web, you will need to proxy/forward `/admin` HTTP requests to port 4567 to let the admin web server handle it. As an alternative, you could forward a local port with SSH --- you might use this if you didn't want to rely on just HTTP basic auth, which isn't very secure over non-HTTPS connections.
231
+
232
+ # Customising the admin interface
233
+
234
+ The admin interface is intended to be a minimal place to focus on writing content. You are free to customise the admin interface by creating a stylesheet at `$your_site_directory/css/admin/admin.css`. As an example, if your main site's stylesheet is `/css/style.css`, you can use an `@import` rule to inherit the look-and-feel of your main site editing content and looking at rendered previews.
235
+
236
+
237
+ ```css
238
+ /* Import the main site's CSS to provide a similar look-and-feel for the admin interface */
239
+
240
+ @import url("/css/style.css");
241
+
242
+ /* more customisation below */
243
+ ```
data/bin/serif ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
4
+
5
+ require "fileutils"
6
+ require "slop"
7
+
8
+ def initialize_admin_server(source_dir)
9
+ # need to cd to the directory before requiring the admin
10
+ # server, because otherwise Dir.pwd won't be right when
11
+ # the admin server class is defined at require time.
12
+ FileUtils.cd(source_dir)
13
+ require "serif"
14
+ require "serif/admin_server"
15
+
16
+ server = Serif::AdminServer.new(source_dir)
17
+ server.start
18
+ end
19
+
20
+ def initialize_dev_server(source_dir)
21
+ FileUtils.cd(source_dir)
22
+ require "serif"
23
+ require "serif/server"
24
+
25
+ server = Serif::DevelopmentServer.new(source_dir)
26
+ server.start
27
+ end
28
+
29
+ def generate_site(source_dir)
30
+ require "serif"
31
+
32
+ site = Serif::Site.new(source_dir)
33
+ site.generate
34
+ end
35
+
36
+ def verify_directory(dir)
37
+ unless Dir.exist?(dir)
38
+ puts "No such directory: #{dir}'"
39
+ exit 1
40
+ end
41
+ end
42
+
43
+ def produce_skeleton(dir)
44
+ if !Dir[File.join(dir, "*")].empty?
45
+ abort "Directory is not empty."
46
+ end
47
+
48
+ FileUtils.cd(File.join(File.dirname(__FILE__), "..", "statics", "skeleton"))
49
+ files = Dir["*"]
50
+ files.each do |f|
51
+ FileUtils.cp_r(f, dir, verbose: true)
52
+ end
53
+ end
54
+
55
+ commands = Slop::Commands.new do
56
+ on :admin do
57
+ add_callback :empty do
58
+ initialize_admin_server(Dir.pwd)
59
+ end
60
+ end
61
+
62
+ on :generate do
63
+ add_callback :empty do
64
+ generate_site(Dir.pwd)
65
+ end
66
+ end
67
+
68
+ on :dev do
69
+ add_callback :empty do
70
+ initialize_dev_server(Dir.pwd)
71
+ end
72
+ end
73
+
74
+ on :new do
75
+ add_callback :empty do
76
+ produce_skeleton(Dir.pwd)
77
+ end
78
+ end
79
+ end
80
+
81
+ commands.parse
@@ -0,0 +1,186 @@
1
+ require "sinatra/base"
2
+ require "fileutils"
3
+
4
+ module Serif
5
+ class AdminServer
6
+ class AdminApp < Sinatra::Base
7
+ Tilt.register :html, Tilt[:liquid]
8
+
9
+
10
+ set :root, Dir.pwd
11
+ set :public_folder, settings.root + (ENV["ENV"] == "production" ? "/_site" : "")
12
+ set :views, File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "statics", "templates", "admin"))
13
+
14
+ site = Serif::Site.new(settings.root)
15
+
16
+ use(Rack::Auth::Basic, "Login credentials") do |username, password|
17
+ [username, password] == [site.config.admin_username, site.config.admin_password]
18
+ end
19
+
20
+ # multiple public folders??
21
+ get "/admin/js/:file" do |file|
22
+ assets_dir = File.join(File.dirname(__FILE__), "..", "..", "statics", "assets", "js")
23
+
24
+ [200, { "Content-Type" => "text/javascript" }, File.read(File.join(assets_dir, file))]
25
+ end
26
+
27
+ get "/" do
28
+ redirect to("/admin")
29
+ end
30
+
31
+ get "/admin/?" do
32
+ posts = site.posts.sort_by { |p| p.created }.reverse
33
+ drafts = site.drafts.sort_by { |p| p.slug }.reverse
34
+
35
+ liquid :index, locals: { posts: posts, drafts: drafts }
36
+ end
37
+
38
+ get "/admin/edit/?" do
39
+ redirect to("/admin"), 301
40
+ end
41
+
42
+ get "/admin/new/draft" do
43
+ content = Draft.new(site)
44
+ autofocus = "slug"
45
+ liquid :new_draft, locals: { post: content, autofocus: autofocus }
46
+ end
47
+
48
+ post "/admin/new/draft" do
49
+ content = Draft.new(site)
50
+ content.slug = params[:slug].strip
51
+ content.title = params[:title].strip
52
+
53
+ if params[:markdown].strip.empty? || params[:title].empty? || params[:slug].empty?
54
+ [:title, :slug, :markdown].each do |p|
55
+ params[p] = nil if params[p] && params[p].empty?
56
+ end
57
+
58
+ error_message = "There must be a URL, a title, and content to save."
59
+
60
+ autofocus = "markdown" unless params[:markdown]
61
+ autofocus = "title" unless params[:title]
62
+ autofocus = "slug" unless params[:slug]
63
+
64
+ liquid :new_draft, locals: { error_message: error_message, post: content, autofocus: autofocus }
65
+ else
66
+ if Draft.exist?(site, params[:slug])
67
+ liquid :new_draft, locals: { error_message: error_message, post: content, autofocus: autofocus }
68
+ else
69
+ content.save(params[:markdown])
70
+ redirect to("/admin")
71
+ end
72
+ end
73
+ end
74
+
75
+ post "/admin/edit/drafts" do
76
+ content = Draft.from_slug(site, params[:original_slug])
77
+
78
+ params[:markdown] = params[:markdown].strip
79
+
80
+ # check if the slug has been edited, i.e., if we're renaming.
81
+ if !params[:slug].empty? && params[:original_slug] && params[:original_slug] != params[:slug]
82
+ if Draft.exist?(site, params[:slug])
83
+ conflicting_name = true
84
+
85
+ # we need to re-edit, so reload but use the original slug name
86
+ # not the new one that was attempted to be saved.
87
+ content = Draft.from_slug(site, params[:original_slug])
88
+ else
89
+ Draft.rename(params[:original_slug], params[:slug])
90
+
91
+ # re-load after the rename
92
+ content = Draft.from_slug(site, params[:slug])
93
+ end
94
+ end
95
+
96
+ # make sure the title is whatever was just submitted
97
+ content.title = params[:title]
98
+
99
+ # any errors
100
+ if conflicting_name || params[:markdown].empty? || params[:slug].empty?
101
+ if conflicting_name
102
+ error_message = "This name is already being used for a draft."
103
+ elsif params[:markdown].empty?
104
+ error_message = "Content must not be blank."
105
+ elsif params[:slug].empty?
106
+ error_message = "You must pick a URL to use"
107
+ end
108
+
109
+ liquid :edit_draft, locals: { error_message: error_message, post: content }
110
+ else
111
+ content.save(params[:markdown])
112
+
113
+ # TODO: move the entire notion of generating a site out into
114
+ # a directory-change-level event.
115
+ if params[:publish] == "yes"
116
+ content.publish!
117
+ site.generate
118
+ end
119
+
120
+ redirect to("/admin")
121
+ end
122
+ end
123
+
124
+ post "/admin/edit/posts" do
125
+ content = Post.from_slug(site, params[:original_slug])
126
+
127
+ params[:markdown] = params[:markdown].strip
128
+ params[:title] = params[:title].strip
129
+
130
+ content.title = params[:title]
131
+
132
+ if params[:markdown].empty? || params[:title].empty?
133
+ error_message = "Content must not be blank." if params[:markdown].empty?
134
+ error_message = "Title must not be blank." if params[:title].empty?
135
+
136
+ liquid :edit_post, locals: { error_message: error_message, post: content }
137
+ else
138
+ content.save(params[:markdown])
139
+ site.generate
140
+
141
+ redirect to("/admin")
142
+ end
143
+ end
144
+
145
+ get "/admin/edit/:type/:slug" do
146
+ redirect to("/admin") unless params[:slug]
147
+
148
+ if params[:type] == "posts"
149
+ content = site.posts.find { |p| p.slug == params[:slug] }
150
+ liquid :edit_post, locals: { post: content, autofocus: "markdown" }
151
+ elsif params[:type] == "drafts"
152
+ content = Draft.from_slug(site, params[:slug])
153
+ liquid :edit_draft, locals: { post: content, autofocus: "markdown" }
154
+ else
155
+ response.status = 404
156
+ return "Nope"
157
+ end
158
+ end
159
+
160
+ post "/admin/delete/?" do
161
+ content = Draft.from_slug(site, params[:original_slug])
162
+ content.delete!
163
+
164
+ redirect to("/admin")
165
+ end
166
+
167
+ post "/admin/convert-markdown/?" do
168
+ content = params["content"]
169
+
170
+ if request.xhr?
171
+ Redcarpet::Markdown.new(Serif::MarkupRenderer, fenced_code_blocks: true).render(content).strip
172
+ end
173
+ end
174
+ end
175
+
176
+ def initialize(source_directory)
177
+ @source_directory = File.expand_path(source_directory)
178
+ end
179
+
180
+ def start
181
+ FileUtils.cd @source_directory
182
+ app = Sinatra.new(AdminApp)
183
+ app.run!
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,25 @@
1
+ require "yaml"
2
+
3
+ module Serif
4
+ class Config
5
+ def initialize(config_file)
6
+ @config_file = config_file
7
+ end
8
+
9
+ def yaml
10
+ YAML.load_file(@config_file)
11
+ end
12
+
13
+ def admin_username
14
+ yaml["admin"]["username"]
15
+ end
16
+
17
+ def admin_password
18
+ yaml["admin"]["password"]
19
+ end
20
+
21
+ def permalink
22
+ yaml["permalink"] || "/:title"
23
+ end
24
+ end
25
+ end