|
| 1 | +;;; org-blog.el --- create and publish a blog with org-mode |
| 2 | + |
| 3 | +;; Copyright (C) 2006 David O'Toole |
| 4 | + |
| 5 | +;; Author: David O'Toole <[email protected]> |
| 6 | +;; Keywords: hypermedia, tools |
| 7 | +;; $Id: org-blog.el,v 1.18 2007/06/13 16:21:24 dto Exp dto $ |
| 8 | + |
| 9 | +;; This file is free software; you can redistribute it and/or modify |
| 10 | +;; it under the terms of the GNU General Public License as published by |
| 11 | +;; the Free Software Foundation; either version 2, or (at your option) |
| 12 | +;; any later version. |
| 13 | + |
| 14 | +;; This file is distributed in the hope that it will be useful, |
| 15 | +;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 16 | +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 17 | +;; GNU General Public License for more details. |
| 18 | + |
| 19 | +;; You should have received a copy of the GNU General Public License |
| 20 | +;; along with GNU Emacs; see the file COPYING. If not, write to |
| 21 | +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| 22 | +;; Boston, MA 02110-1301, USA. |
| 23 | + |
| 24 | +;;; Commentary: |
| 25 | + |
| 26 | +;; This program adds simple blog publishing support to org-mode. It is |
| 27 | +;; built on top of org-publish.el. |
| 28 | + |
| 29 | +;; You should read the documentation for org-publish.el before |
| 30 | +;; continuing. |
| 31 | + |
| 32 | +;; The latest version of this program, and of org-publish.el, can be |
| 33 | +;; found at: http://dto.freeshell.org/notebook/OrgMode.html |
| 34 | + |
| 35 | +;;;; 1. Basic configuration |
| 36 | +;; |
| 37 | +;; First add (require 'org-blog) to your emacs initialization file. |
| 38 | +;; |
| 39 | +;; Then set the variable org-blog-directory (you can also leave it |
| 40 | +;; as the default, "~/blog/"). This directory should be different |
| 41 | +;; from the directory where your normal *.org files are stored, |
| 42 | +;; otherwise they will get "posted". |
| 43 | +;; |
| 44 | +;; You should also set the variable |
| 45 | +;; org-blog-unfinished-directory. The default is |
| 46 | +;; "~/blog/unfinished". This is the directory where unfinished |
| 47 | +;; posts are stored. You can leave posts in the unfinished |
| 48 | +;; directory while you are working on them, and they won't be |
| 49 | +;; published. |
| 50 | + |
| 51 | +;;;; 2. Create a post |
| 52 | +;; |
| 53 | +;; Use M-x org-blog-new-post. You'll be prompted for a |
| 54 | +;; filename. Enter a short name for this post (without the ".org") |
| 55 | +;; and press RET. You'll see a new buffer with a blank TITLE field. |
| 56 | + |
| 57 | +;; You can work on more than one post at once. They'll all be |
| 58 | +;; stored in your `org-blog-unfinished-directory'. To view a list |
| 59 | +;; of posts in progress, use M-x |
| 60 | +;; org-blog-find-unfinished-posts. You'll see the directory listing |
| 61 | +;; of `org-blog-unfinished-directory', and you can use RET to |
| 62 | +;; select a post to edit. |
| 63 | + |
| 64 | +;;;; 3. Finish a post |
| 65 | +;; |
| 66 | +;; When your post is ready, visit the file and hit M-x |
| 67 | +;; org-blog-finish-post. This does not mean the post is published |
| 68 | +;; on your website, only that the post is "finished" and given a |
| 69 | +;; timestamped filename. Your blog post and updated index will be |
| 70 | +;; published when you execute M-x org-publish-all. |
| 71 | +;; |
| 72 | +;; But first, let's take a detour to make sure blog publishing is |
| 73 | +;; set up properly. |
| 74 | + |
| 75 | +;;;; 4. Configure blog publishing |
| 76 | +;; |
| 77 | +;; Org-blog contains an index function to publish a front page for |
| 78 | +;; your blog. This index can be configured to display the most |
| 79 | +;; recent posts, and your "blogroll" or list of links to other |
| 80 | +;; blogs. The newest post will always be at the top. |
| 81 | +;; |
| 82 | +;; You should add a project called "blog" to your |
| 83 | +;; org-publish-project-alist. Here is an example project |
| 84 | +;; configuration you can adapt to your needs: |
| 85 | + |
| 86 | +;; '("blog" :base-directory "~/blog/" |
| 87 | +;; :base-extension "org" |
| 88 | +;; :publishing-directory "/protocol:user@host:~/html/blog/" |
| 89 | +;; :publishing-function org-publish-org-to-html |
| 90 | +;; :auto-index t |
| 91 | +;; :blog-base-url "http://dto.freeshell.org/blog/" |
| 92 | +;; :blog-title "dto.freeshell.org blog" |
| 93 | +;; :blog-description "David O'Toole's web log." |
| 94 | +;; :blog-export-rss t |
| 95 | +;; :index-function org-publish-blog-index |
| 96 | +;; :index-filename "index.org" |
| 97 | +;; :index-title "Title of my Blog" |
| 98 | +;; :index-posts 2 |
| 99 | +;; :preamble my-blogroll-html |
| 100 | +;; :postamble my-footer-html) |
| 101 | + |
| 102 | +;; Most of these keywords are documented along with |
| 103 | +;; org-publish-project-alist. Before moving on, we'll explain |
| 104 | +;; usages specific to blogging support. |
| 105 | + |
| 106 | +;; The keyword :index-posts controls how many posts will be shown |
| 107 | +;; on the blog's front page. Set its value to an integer. Remaining |
| 108 | +;; posts will be shown as a list of links at the bottom of the |
| 109 | +;; page. |
| 110 | + |
| 111 | +;; The :index-title should be used to set the title of your blog. |
| 112 | +;; You can use the standard :preamble and :postamble keywords to |
| 113 | +;; set the header and footer of your blog posts and front page. |
| 114 | +;; This is a great place to include your HTML blogroll and |
| 115 | +;; copyright notices. |
| 116 | + |
| 117 | +;;;; 5. Now publish! |
| 118 | +;; |
| 119 | +;; After you've updated your org-publish-project-alist and created |
| 120 | +;; a post or two, hit M-x org-publish-all. Your posts should be |
| 121 | +;; uploaded, and an index frontpage generated. |
| 122 | + |
| 123 | + |
| 124 | +;;; Code: |
| 125 | + |
| 126 | +(require 'org-publish) |
| 127 | + |
| 128 | +(defgroup org-blog nil |
| 129 | + "Options for keeping and publishing a blog with org-mode." |
| 130 | + :tag "Org Blog" |
| 131 | + :group 'org-publish) |
| 132 | + |
| 133 | +(defcustom org-blog-directory "~/blog/" |
| 134 | + "Directory where finished blog posts are stored." |
| 135 | + :group 'org-blog) |
| 136 | + |
| 137 | +(defcustom org-blog-unfinished-directory "~/blog/unfinished" |
| 138 | + "Directory where unfinished posts are stored." |
| 139 | + :group 'org-blog) |
| 140 | + |
| 141 | +(defcustom org-blog-time-format "%Y-%m-%d %I:%M%p -- " |
| 142 | + "Format string used when timestamping posts." |
| 143 | + :group 'org-blog) |
| 144 | + |
| 145 | +(defun org-blog-new-post-file () |
| 146 | + (concat (file-name-as-directory org-blog-directory) (format-time-string "blog-%Y-%m-%d-%H%M.org"))) |
| 147 | + |
| 148 | +(defun org-blog-new-post (filename) |
| 149 | + "Create a new post in FILENAME. |
| 150 | +Post is stored in `org-blog-unfinished-directory'." |
| 151 | + (interactive "sFilename for new post: ") |
| 152 | + (find-file (concat (file-name-as-directory org-blog-unfinished-directory) |
| 153 | + filename ".org")) |
| 154 | + (insert "#+TITLE: \n") |
| 155 | + (insert "#+DESCRIPTION: ")) |
| 156 | + |
| 157 | +(defun org-blog-find-unfinished-posts () |
| 158 | + "Open `org-blog-unfinished-directory'." |
| 159 | + (interactive) |
| 160 | + (let ((dir (file-name-as-directory org-blog-unfinished-directory))) |
| 161 | + (when (not (file-exists-p dir)) |
| 162 | + (make-directory dir t)) |
| 163 | + (find-file dir))) |
| 164 | + |
| 165 | +(defun org-blog-finish-post () |
| 166 | + "Complete and timestamp the unfinished post in the current buffer. |
| 167 | +Follow up with org-publish-all to upload to the site." |
| 168 | + (interactive) |
| 169 | + (write-file (org-blog-new-post-file))) |
| 170 | + |
| 171 | +;; pluggable index generation function for org-publish. |
| 172 | + |
| 173 | +(defun org-publish-blog-index (plist &optional index-filename) |
| 174 | + "Publish an index of all finished blog posts. |
| 175 | +This function is suitable for use in the :index-function keyword |
| 176 | +of org-publish-project-alist." |
| 177 | + (let* ((posts (nreverse (sort (org-publish-get-base-files plist "*~") 'string<))) |
| 178 | + (base-directory (file-name-as-directory (or org-blog-directory (plist-get plist :base-directory)))) |
| 179 | + (blog-base-url (file-name-as-directory (plist-get plist :blog-base-url))) |
| 180 | + (blog-title (plist-get plist :blog-title)) |
| 181 | + (publishing-directory (file-name-as-directory |
| 182 | + (plist-get plist :publishing-directory))) |
| 183 | + (blog-description (plist-get plist :blog-description)) |
| 184 | + (blog-rss-feed nil) |
| 185 | + (rss (plist-get plist :blog-export-rss)) |
| 186 | + (post-content nil) |
| 187 | + (index-file (concat base-directory (or index-filename "index.org"))) |
| 188 | + (index-buffer (find-buffer-visiting index-file)) |
| 189 | + (num-posts (or (plist-get plist :index-posts) 5)) |
| 190 | + (index-title (plist-get plist :index-title)) |
| 191 | + (count 0) |
| 192 | + (p nil)) |
| 193 | + |
| 194 | + (message "RSS = %S" rss) |
| 195 | + ;; |
| 196 | + ;; if buffer is already open, kill it |
| 197 | + (if index-buffer |
| 198 | + (kill-buffer index-buffer)) |
| 199 | + ;; |
| 200 | + ;; start the RSS feed |
| 201 | + (when rss |
| 202 | + (push (org-blog-rss-preamble blog-title blog-base-url blog-description) |
| 203 | + blog-rss-feed)) |
| 204 | + ;; |
| 205 | + (with-temp-buffer |
| 206 | + ;; |
| 207 | + ;; process each post |
| 208 | + (while (setq p (pop posts)) |
| 209 | + (let ((basename (file-name-nondirectory p)) |
| 210 | + (post-title nil) |
| 211 | + (post-time (format-time-string |
| 212 | + "%a, %d %b %Y %H:%M:00 %z" |
| 213 | + (nth 5 (file-attributes p)))) |
| 214 | + (post-description nil)) |
| 215 | + ;; |
| 216 | + ;; grab post details |
| 217 | + (with-temp-buffer |
| 218 | + (insert-file-contents p) |
| 219 | + ;; |
| 220 | + ;; make sure we are in org-mode (otherwise export won't work properly) |
| 221 | + (let ((org-inhibit-startup t)) (org-mode)) |
| 222 | + (goto-char (point-min)) |
| 223 | + (re-search-forward "#\\+TITLE: \\(.*\\)$" nil t) |
| 224 | + (setf post-title (match-string 1)) |
| 225 | + (re-search-forward "#\\+DESCRIPTION: \\(.*\\)$" nil t) |
| 226 | + (setf post-description (match-string 1)) |
| 227 | + (setf post-content (buffer-substring-no-properties |
| 228 | + (match-end 1) (point-max)))) |
| 229 | + ;; |
| 230 | + ;; avoid inserting existing index; this would be a loop! |
| 231 | + (when (not (string= basename index-filename)) |
| 232 | + ;; |
| 233 | + ;; add rss item |
| 234 | + (when rss |
| 235 | + (push (org-blog-rss-item post-title |
| 236 | + (concat blog-base-url |
| 237 | + (file-name-sans-extension |
| 238 | + (file-name-nondirectory p)) |
| 239 | + ".html") |
| 240 | + post-content |
| 241 | + post-time) |
| 242 | + blog-rss-feed)) |
| 243 | + (if (< count num-posts) |
| 244 | + ;; |
| 245 | + ;; insert full text of post |
| 246 | + (progn (insert-file-contents p) |
| 247 | + ;; permalink |
| 248 | + (goto-char (point-max)) |
| 249 | + (insert (with-temp-buffer |
| 250 | + (insert (concat "\n\n [[file:" basename "][Permalink]]\n\n")) |
| 251 | + (buffer-substring-no-properties (point-min) (point-max))))) |
| 252 | + |
| 253 | + ;; |
| 254 | + ;; or, just insert link with title |
| 255 | + (progn |
| 256 | + (goto-char (point-max)) |
| 257 | + (when (= count num-posts) |
| 258 | + (insert "\n** Older posts\n")) |
| 259 | + (insert (concat " - [[file:" |
| 260 | + basename "][" |
| 261 | + post-title "]]\n"))))) |
| 262 | + (setq count (+ 1 count)))) |
| 263 | + ;; |
| 264 | + ;; finish rss feed and write |
| 265 | + (when rss |
| 266 | + (push (org-blog-rss-postamble) blog-rss-feed) |
| 267 | + (with-temp-buffer |
| 268 | + (apply 'insert (nreverse blog-rss-feed)) |
| 269 | + (message "%S - %S" |
| 270 | + (concat publishing-directory "blog.rss") |
| 271 | + blog-rss-feed) |
| 272 | + |
| 273 | + (write-file (concat publishing-directory "blog.xml")))) |
| 274 | + ;; |
| 275 | + ;; turn pasted titles into headings |
| 276 | + (goto-char (point-min)) |
| 277 | + (while (search-forward "#+TITLE: " nil t) |
| 278 | + (replace-match "** " nil t)) |
| 279 | + ;; |
| 280 | + ;; insert index title, if any |
| 281 | + (when index-title |
| 282 | + (goto-char (point-min)) |
| 283 | + (insert (concat "#+TITLE: " index-title "\n\n"))) |
| 284 | + (write-file index-file) |
| 285 | + (kill-buffer (current-buffer))))) |
| 286 | + |
| 287 | + |
| 288 | +;;;; minimal RSS 2.0 support |
| 289 | + |
| 290 | + |
| 291 | +(defun org-blog-rss-preamble (title link description) |
| 292 | + (format |
| 293 | +"<rss version=\"2.0\"> |
| 294 | + <channel> |
| 295 | + <title>%s</title> |
| 296 | + <link>%s</link> |
| 297 | + <description>%s</description> |
| 298 | + <generator>OrgBlog</generator>" |
| 299 | + title link description)) |
| 300 | + |
| 301 | + |
| 302 | +(defun org-blog-rss-postamble () |
| 303 | + "</channel></rss>") |
| 304 | + |
| 305 | + |
| 306 | +(defun org-blog-rss-item (title permalink description pubdate) |
| 307 | + (let ((description-html (with-temp-buffer |
| 308 | + (insert description) |
| 309 | + (org-export-region-as-html (point-min) (point-max) |
| 310 | + :body-only 'string)))) |
| 311 | + (format |
| 312 | + " <item> |
| 313 | + <title>%s</title> |
| 314 | + <description>%s</description> |
| 315 | + <pubDate>%s</pubDate> |
| 316 | + <guid isPermaLink=\"true\">%s</guid> |
| 317 | + </item>\n" title description-html pubdate permalink))) |
| 318 | + |
| 319 | + |
| 320 | + |
| 321 | + |
| 322 | +(provide 'org-blog) |
| 323 | +;;; org-blog.el ends here |
0 commit comments