;;; org-blog.el --- create and publish a blog with org-mode ;; Copyright (C) 2006 David O'Toole ;; Author: David O'Toole <dto@gnu.org> ;; Keywords: hypermedia, tools ;; $Id: org-blog.el,v 1.18 2007/06/13 16:21:24 dto Exp dto $ ;; This file is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 2, or (at your option) ;; any later version. ;; This file is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; This program adds simple blog publishing support to org-mode. It is ;; built on top of org-publish.el. ;; You should read the documentation for org-publish.el before ;; continuing. ;; The latest version of this program, and of org-publish.el, can be ;; found at: http://dto.freeshell.org/notebook/OrgMode.html ;;;; 1. Basic configuration ;; ;; First add (require 'org-blog) to your emacs initialization file. ;; ;; Then set the variable org-blog-directory (you can also leave it ;; as the default, "~/blog/"). This directory should be different ;; from the directory where your normal *.org files are stored, ;; otherwise they will get "posted". ;; ;; You should also set the variable ;; org-blog-unfinished-directory. The default is ;; "~/blog/unfinished". This is the directory where unfinished ;; posts are stored. You can leave posts in the unfinished ;; directory while you are working on them, and they won't be ;; published. ;;;; 2. Create a post ;; ;; Use M-x org-blog-new-post. You'll be prompted for a ;; filename. Enter a short name for this post (without the ".org") ;; and press RET. You'll see a new buffer with a blank TITLE field. ;; You can work on more than one post at once. They'll all be ;; stored in your `org-blog-unfinished-directory'. To view a list ;; of posts in progress, use M-x ;; org-blog-find-unfinished-posts. You'll see the directory listing ;; of `org-blog-unfinished-directory', and you can use RET to ;; select a post to edit. ;;;; 3. Finish a post ;; ;; When your post is ready, visit the file and hit M-x ;; org-blog-finish-post. This does not mean the post is published ;; on your website, only that the post is "finished" and given a ;; timestamped filename. Your blog post and updated index will be ;; published when you execute M-x org-publish-all. ;; ;; But first, let's take a detour to make sure blog publishing is ;; set up properly. ;;;; 4. Configure blog publishing ;; ;; Org-blog contains an index function to publish a front page for ;; your blog. This index can be configured to display the most ;; recent posts, and your "blogroll" or list of links to other ;; blogs. The newest post will always be at the top. ;; ;; You should add a project called "blog" to your ;; org-publish-project-alist. Here is an example project ;; configuration you can adapt to your needs: ;; '("blog" :base-directory "~/blog/" ;; :base-extension "org" ;; :publishing-directory "/protocol:user@host:~/html/blog/" ;; :publishing-function org-publish-org-to-html ;; :auto-index t ;; :blog-base-url "http://dto.freeshell.org/blog/" ;; :blog-title "dto.freeshell.org blog" ;; :blog-description "David O'Toole's web log." ;; :blog-export-rss t ;; :index-function org-publish-blog-index ;; :index-filename "index.org" ;; :index-title "Title of my Blog" ;; :index-posts 2 ;; :preamble my-blogroll-html ;; :postamble my-footer-html) ;; Most of these keywords are documented along with ;; org-publish-project-alist. Before moving on, we'll explain ;; usages specific to blogging support. ;; The keyword :index-posts controls how many posts will be shown ;; on the blog's front page. Set its value to an integer. Remaining ;; posts will be shown as a list of links at the bottom of the ;; page. ;; The :index-title should be used to set the title of your blog. ;; You can use the standard :preamble and :postamble keywords to ;; set the header and footer of your blog posts and front page. ;; This is a great place to include your HTML blogroll and ;; copyright notices. ;;;; 5. Now publish! ;; ;; After you've updated your org-publish-project-alist and created ;; a post or two, hit M-x org-publish-all. Your posts should be ;; uploaded, and an index frontpage generated. ;;; Code: (require 'org-publish) (defgroup org-blog nil "Options for keeping and publishing a blog with org-mode." :tag "Org Blog" :group 'org-publish) (defcustom org-blog-directory "~/blog/" "Directory where finished blog posts are stored." :group 'org-blog) (defcustom org-blog-unfinished-directory "~/blog/unfinished" "Directory where unfinished posts are stored." :group 'org-blog) (defcustom org-blog-time-format "%Y-%m-%d %I:%M%p -- " "Format string used when timestamping posts." :group 'org-blog) (defun org-blog-new-post-file () (concat (file-name-as-directory org-blog-directory) (format-time-string "blog-%Y-%m-%d-%H%M.org"))) (defun org-blog-new-post (filename) "Create a new post in FILENAME. Post is stored in `org-blog-unfinished-directory'." (interactive "sFilename for new post: ") (find-file (concat (file-name-as-directory org-blog-unfinished-directory) filename ".org")) (insert "#+TITLE: \n") (insert "#+DESCRIPTION: ")) (defun org-blog-find-unfinished-posts () "Open `org-blog-unfinished-directory'." (interactive) (let ((dir (file-name-as-directory org-blog-unfinished-directory))) (when (not (file-exists-p dir)) (make-directory dir t)) (find-file dir))) (defun org-blog-finish-post () "Complete and timestamp the unfinished post in the current buffer. Follow up with org-publish-all to upload to the site." (interactive) (write-file (org-blog-new-post-file))) ;; pluggable index generation function for org-publish. (defun org-publish-blog-index (plist &optional index-filename) "Publish an index of all finished blog posts. This function is suitable for use in the :index-function keyword of org-publish-project-alist." (let* ((posts (nreverse (sort (org-publish-get-base-files plist "*~") 'string<))) (base-directory (file-name-as-directory (or org-blog-directory (plist-get plist :base-directory)))) (blog-base-url (file-name-as-directory (plist-get plist :blog-base-url))) (blog-title (plist-get plist :blog-title)) (publishing-directory (file-name-as-directory (plist-get plist :publishing-directory))) (blog-description (plist-get plist :blog-description)) (blog-rss-feed nil) (rss (plist-get plist :blog-export-rss)) (post-content nil) (index-file (concat base-directory (or index-filename "index.org"))) (index-buffer (find-buffer-visiting index-file)) (num-posts (or (plist-get plist :index-posts) 5)) (index-title (plist-get plist :index-title)) (count 0) (p nil)) (message "RSS = %S" rss) ;; ;; if buffer is already open, kill it (if index-buffer (kill-buffer index-buffer)) ;; ;; start the RSS feed (when rss (push (org-blog-rss-preamble blog-title blog-base-url blog-description) blog-rss-feed)) ;; (with-temp-buffer ;; ;; process each post (while (setq p (pop posts)) (let ((basename (file-name-nondirectory p)) (post-title nil) (post-time (format-time-string "%a, %d %b %Y %H:%M:00 %z" (nth 5 (file-attributes p)))) (post-description nil)) ;; ;; grab post details (with-temp-buffer (insert-file-contents p) ;; ;; make sure we are in org-mode (otherwise export won't work properly) (let ((org-inhibit-startup t)) (org-mode)) (goto-char (point-min)) (re-search-forward "#\\+TITLE: \\(.*\\)$" nil t) (setf post-title (match-string 1)) (re-search-forward "#\\+DESCRIPTION: \\(.*\\)$" nil t) (setf post-description (match-string 1)) (setf post-content (buffer-substring-no-properties (match-end 1) (point-max)))) ;; ;; avoid inserting existing index; this would be a loop! (when (not (string= basename index-filename)) ;; ;; add rss item (when rss (push (org-blog-rss-item post-title (concat blog-base-url (file-name-sans-extension (file-name-nondirectory p)) ".html") post-content post-time) blog-rss-feed)) (if (< count num-posts) ;; ;; insert full text of post (progn (insert-file-contents p) ;; permalink (goto-char (point-max)) (insert (with-temp-buffer (insert (concat "\n\n [[file:" basename "][Permalink]]\n\n")) (buffer-substring-no-properties (point-min) (point-max))))) ;; ;; or, just insert link with title (progn (goto-char (point-max)) (when (= count num-posts) (insert "\n** Older posts\n")) (insert (concat " - [[file:" basename "][" post-title "]]\n"))))) (setq count (+ 1 count)))) ;; ;; finish rss feed and write (when rss (push (org-blog-rss-postamble) blog-rss-feed) (with-temp-buffer (apply 'insert (nreverse blog-rss-feed)) (message "%S - %S" (concat publishing-directory "blog.rss") blog-rss-feed) (write-file (concat publishing-directory "blog.xml")))) ;; ;; turn pasted titles into headings (goto-char (point-min)) (while (search-forward "#+TITLE: " nil t) (replace-match "** " nil t)) ;; ;; insert index title, if any (when index-title (goto-char (point-min)) (insert (concat "#+TITLE: " index-title "\n\n"))) (write-file index-file) (kill-buffer (current-buffer))))) ;;;; minimal RSS 2.0 support (defun org-blog-rss-preamble (title link description) (format "<rss version=\"2.0\"> <channel> <title>%s</title> <link>%s</link> <description>%s</description> <generator>OrgBlog</generator>" title link description)) (defun org-blog-rss-postamble () "</channel></rss>") (defun org-blog-rss-item (title permalink description pubdate) (let ((description-html (with-temp-buffer (insert description) (org-export-region-as-html (point-min) (point-max) :body-only 'string)))) (format " <item> <title>%s</title> <description>%s</description> <pubDate>%s</pubDate> <guid isPermaLink=\"true\">%s</guid> </item>\n" title description-html pubdate permalink))) (provide 'org-blog) ;;; org-blog.el ends here