├── LICENSE ├── README.org └── org-static-blog.el /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, Bastian Bechtold 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the 13 | distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: ORG-STATIC-BLOG 2 | 3 | [[http://melpa.org/packages/org-static-blog-badge.svg]] [[http://stable.melpa.org/packages/org-static-blog-badge.svg]] 4 | 5 | 6 | Static blog generators are a dime a dozen. This is one more, which 7 | focuses on being simple. All files are simple org-mode files in a 8 | directory. The only requirement is that every org file must have a 9 | =#+TITLE= and a =#+DATE=, and optionally, =#+FILETAGS=, =#+DESCRIPTION= and 10 | =#+IMAGE=. 11 | 12 | =#+FILETAGS= set the tags for the post, which can be delimited by 13 | colons in the form of =#+FILETAGS: :foo bar:baz:=, or by whitespaces 14 | in the form of =#+FILETAGS: foo bar=. And you can put a short summary 15 | in =#+DESCRIPTION=, which will be converted to a HTML 16 | ~~ tag, it's good for SEO as described in 17 | Wikipedia's [[https://en.wikipedia.org/wiki/Meta_element#The_description_attribute][Meta element]]. 18 | 19 | If =org-static-blog-enable-og-tags= is set to =t=, =#+DESCRIPTION= and 20 | =#+IMAGE= (it must be e a relative url) will generate the =og:description= 21 | and =og:image= meta properties that will be used in URL previews 22 | generated in social networks and similar sites (see [[https://ogp.me/][Open Graph 23 | Protocol]]). 24 | 25 | This file is also available from marmalade and melpa-stable. 26 | 27 | Set up your blog by customizing org-static-blog's parameters, then 28 | call =M-x org-static-blog-publish= to render the whole blog or 29 | =M-x org-static-blog-publish-file filename.org= to render only only 30 | the file =filename.org=. 31 | 32 | Above all, I tried to make org-static-blog as simple as possible. 33 | There are no magic tricks, and all of the source code is meant to be 34 | easy to read, understand and modify. 35 | 36 | For org-static-blog, a blog consists of six parts: 37 | - Blog posts contain individual entries. Every org file in 38 | =org-static-blog-posts-directory= is one blog post. Each blog post 39 | is rendered as its own HTML page. 40 | - The index page contains the last few blog posts on a single page. 41 | The number of entries on the index page can be customized using 42 | =org-static-blog-index-length=. 43 | - Optionally show a preview of the post (instead of the full post) on 44 | the index page setting =org-static-blog-use-preview= to t. The region 45 | of the post used as a preview is, by default, its first paragraph, 46 | but can be fine-tuned using =org-static-blog-preview-start= and 47 | =org-static-blog-preview-end.= 48 | - The archive page lists the publishing dates and headlines of every 49 | blog post. 50 | - The RSS feed is a machine-readable XML file that contains every blog 51 | post. It is not meant to be consumed by humans. Instead RSS readers 52 | can use the RSS feed to aggregate entries from multiple blogs. 53 | - Drafts are rendered like regular blog posts, but are not included in 54 | the index, the archive, or the RSS feed. 55 | - Each blog post can be tagged, and each tag links to a page that 56 | lists all other posts of the same tag. Additionally, a tag overview 57 | page is created that lists the publishing dates and headlines of 58 | every blog post, sorted by tags. This feature is only enabled if you 59 | set =org-static-blog-enable-tags= to =t=. 60 | - If =org-static-blog-enable-og-tags= is set to =t=, all generated pages 61 | will include some useful [[https://ogp.me/][Open Graph]] meta properties such as the 62 | title, the description and an optional image. If no =#+IMAGE= property 63 | is provided in a post, the default one specified in 64 | =org-static-blog-image= will be used. 65 | - To disable comments for single blog posts, reserve a tag name as 66 | =org-static-blog-no-comments-tag= and tag the post with that tag. 67 | - It is also possible to create per-tag RSS feeds. This feature is 68 | only enabled when you set =org-static-blog-enable-tag-rss= to t. 69 | 70 | Every HTML page in org-static-blog can be customized in the following 71 | ways: 72 | - The contents of =org-static-blog-page-header= are inserted into the 73 | == of every page. Use this to include custom CSS and 74 | JavaScript for your blog. 75 | - The contents of =org-static-blog-page-preamble= is inserted just 76 | before the content of every page. This is a good place to put the 77 | header or menus for your blog. 78 | - The contents of =org-static-blog-page-postamble= is inserted after 79 | the content of every generated page: after any blog post page, after 80 | the index page, the tag pages and the archive. This is where you can 81 | include copyright notices. 82 | - The return values of =org-static-blog-post-preamble= and 83 | =org-static-blog-post-postamble= are prepended and appended to every 84 | blog post. If you want to change the formatting of dates, titles, or 85 | the tag list, overwrite these functions. In particular the content 86 | of =org-static-blog-post-comments= is inserted at the end of each 87 | blog post. Use this to add a comment box. 88 | 89 | You can customize the RSS feed output by setting 90 | =org-static-blog-rss-extra=. Its content is placed right before the 91 | sequence of posts. For example you can add an RSS icon for the feed, 92 | or advertise that you built your blog with org-static-blog. You can 93 | also limit the number of entries in the feed via 94 | =org-static-blog-rss-max-entries=. 95 | 96 | 97 | There are some static texts like "/Other posts/", "/Tags/" etc that 98 | org-static-blog includes in produced html. By default org-static-blog 99 | uses english texts, but language chosen depends on value set to 100 | =org-static-blog-langcode=. If your language is not supported yet, you 101 | will see placeholders like =[other-posts:de]= and =[tags:de]=. 102 | You can add new language by adding texts to =org-static-blog-texts= 103 | list. And if you do, please share and create Pull Request. 104 | 105 | If you want to activate a few convenience key bindings, add 106 | =(add-to-list 'auto-mode-alist (cons (concat org-static-blog-posts-directory ".*\\.org\\'") 'org-static-blog-mode))= 107 | to your /init.el/. These key bindings are: 108 | - =C-c C-f= / =C-c C-b= to open next/previous post. 109 | - =C-c C-p= to open the matching published HTML file of a post. 110 | - =C-c C-n= to create a new blog post. 111 | 112 | 113 | If you have questions, if you find bugs, or if you would like to 114 | contribute something to org-static-blog, please open an issue or pull 115 | request on GitHub. 116 | 117 | Finally, I would like to remind you that I am developing this project 118 | for free, and in my spare time. While I try to be as accommodating as 119 | possible, I can not guarantee a timely response to issues. Publishing 120 | Open Source Software on GitHub does not imply an obligation to /fix 121 | your problem right now/. Please be civil. 122 | 123 | * Examples 124 | 125 | ** Minimal Configuration 126 | This minimal configuration should be added to your /init.el/, and will 127 | set up a minimal org-static-blog for the URL https://staticblog.org, 128 | which will be saved in the directory ~/projects/blog/. 129 | 130 | #+begin_src elisp 131 | (setq org-static-blog-publish-title "My Static Org Blog") 132 | (setq org-static-blog-publish-url "https://staticblog.org/") 133 | (setq org-static-blog-publish-directory "~/projects/blog/") 134 | (setq org-static-blog-posts-directory "~/projects/blog/posts/") 135 | (setq org-static-blog-drafts-directory "~/projects/blog/drafts/") 136 | (setq org-static-blog-enable-tags t) 137 | (setq org-export-with-toc nil) 138 | (setq org-export-with-section-numbers nil) 139 | 140 | ;; This header is inserted into the section of every page: 141 | ;; (you will need to create the style sheet at 142 | ;; ~/projects/blog/static/style.css 143 | ;; and the favicon at 144 | ;; ~/projects/blog/static/favicon.ico) 145 | (setq org-static-blog-page-header 146 | " 147 | 148 | 149 | 150 | ") 151 | 152 | ;; This preamble is inserted at the beginning of the of every page: 153 | ;; This particular HTML creates a
with a simple linked headline 154 | (setq org-static-blog-page-preamble 155 | "
156 | My Static Org Blog 157 |
") 158 | 159 | ;; This postamble is inserted at the end of the of every page: 160 | ;; This particular HTML creates a
with a link to the archive page 161 | ;; and a licensing stub. 162 | (setq org-static-blog-page-postamble 163 | "
164 | Other posts 165 |
166 |
\"Creative
bastibe.de by Bastian Bechtold is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
") 167 | 168 | ;; This HTML code is inserted into the index page between the preamble and 169 | ;; the blog posts 170 | (setq org-static-blog-index-front-matter 171 | "

Welcome to my blog

\n") 172 | #+end_src 173 | 174 | In order for this to work, you will also need to create a style sheet 175 | at /~/projects/blog/static/style.css/, which might for example change 176 | the appearance of the ~#preamble~, the ~#content~, and the 177 | ~#postamble~. 178 | 179 | To write posts, you can now call ~org-static-blog-create-new-post~, 180 | and render your blog with ~org-static-blog-publish~. 181 | 182 | Each post is an org-mode file such as 183 | 184 | #+begin_src org-mode 185 | #+title: How to Write a Blog Post 186 | #+date: <2020-07-03 08:57> 187 | #+filetags: computers emacs blog 188 | 189 | Step one: Install ~org-static-blog~. \\ 190 | Step Two: Execute ~M-x org-static-blog-create-new-post~ and write the content. \\ 191 | Step Three: Execute ~M-x org-static-blog-publish~ and upload to your webhost. \\ 192 | Done. 193 | #+end_src 194 | 195 | You can find more complete examples by looking at my [[https://github.com/bastibe/.emacs.d/blob/master/init.el#L670][init.el]] and the 196 | [[https://github.com/bastibe/bastibe.github.com][repository]] for my blog ([[http://bastibe.de/][bastibe.de]]) itself to see an example of how to 197 | use =org-static-blog= in practice. 198 | 199 | *** Other org-static-blog blogs: 200 | - [[https://zngguvnf.org/][zngguvnf.org]] ---see the [[https://zngguvnf.org/2017-07-13--blogging-with-org-static-blog.html][writeup]] 201 | - [[https://matthewbauer.us/blog/][matthewbauer.us/blog/]] 202 | - [[https://jao.io/blog/simplicity-pays-off.html][jao's programming musings]] 203 | - [[https://f-santos.gitlab.io/][f-santos.gitlab.io]] 204 | - [[https://xgqt.gitlab.io/blog/][xgqt.gitlab.io/blog]] 205 | - [[https://unmonoqueteclea.github.io/][unmonoqueteclea]] 206 | - [[https://chenyo-17.github.io/org-static-blog/][chenyo-17.github.io/org-static-blog]] 207 | - [[https://dou-meishi.github.io/org-blog/][doumeishi.github.io/org-blog]] --- see the [[https://dou-meishi.github.io/org-blog/2024-01-22-TryOrgStaticBlog/notes.html][writeup]] 208 | - Please open a pull request to add your blog, here! 209 | 210 | ** Features 211 | *** Hide some subtrees when publishing 212 | - Background 213 | When publishing some posts, we may not want to publish the more private or unfinished parts of the subtrees. So maybe we can use tags to identify these subtrees and ignore them during the posting process. 214 | - Usage 215 | - Set the corresponding tags 216 | Set the tag to ignore subtrees with =org-static-blog-no-post-tag=, default is =nonpost=. 217 | - Posting 218 | The parts containing this tag will be automatically ignored during the posting process. 219 | - Example 220 | - If you have an org-mode file containing =tree-1=, =tree-2=, =tree-3=. and only want to publish =tree-1= and =tree-3=, then the file would look like this 221 | #+begin_src org-mode 222 | * tree-1 223 | * tree-2 :nonpost: 224 | * tree-3 225 | #+end_src 226 | - Then the file will automatically ignore =tree-2= subtrees with the =nonpost= tag when it is published. 227 | 228 | *** Extended cleaning of the suggested filename 229 | 230 | If you regularly find yourself editing the suggested filename when creating new 231 | posts, e.g. replacing slashes (`/`), then you can modify the value of 232 | `org-static-blog-suggested-filename-cleaning-regexp`. With its default value, 233 | `"\s"`, it only replaces whitespace. 234 | 235 | For example, a post with the title "Using bastibe/org-static-blog for your blog" 236 | would, with the default result in a suggested filename of 237 | `2023-10-02-using-bastibe/org-static-blog-for-your-blog.org`. If the value of 238 | `org-static-blog-suggested-filename-cleaning-regexp` is changed like this 239 | 240 | ``` 241 | (setq org-static-blog-suggested-filename-cleaning-regexp (rx (or "/" (in white))) 242 | ``` 243 | 244 | the `/` will be replaced too and the suggested filename will be 245 | `2023-10-02-using-bastibe-org-static-blog-for-your-blog.org`. 246 | 247 | * Known Issues 248 | 249 | - Org-static-blog is a pure static site generator. As such, it does 250 | not include comments. However, you can easily include services like 251 | Disqus to do this for you. 252 | - You can have hosting services like GitHub auto-render you blog every 253 | time you commit using continuous integration tools like Travis CI. 254 | An example of how to do this has been gracefully provided 255 | by [[https://gitlab.com/_zngguvnf/org-static-blog-example][zngguvnf]]. 256 | - Individual blog entries are only re-rendered if no current HTML file 257 | is available (i.e. the org file is older than the HTML file). If you 258 | want to forcibly re-render an entry, delete the HTML file. 259 | 260 | * Changelog 261 | 262 | - 2018-03-17 (v1.0.4): Massive speed up of org-static-blog. A 263 | re-render with one changed file used to take about a second per 264 | post, and now takes about a second total. 265 | - 2018-03-21 (v1.1.0): Tags. 266 | Each post can now have tags (using =#+tags:=). If you enable 267 | =org-static-blog-enable-tags=, tags are included in each post, 268 | tag-index pages are generated for each tag, and a tag archive 269 | is generated for all tags. 270 | - 2018-03-23 (v1.1.1): Tags. 271 | Deprecated =#+tags:= in favor of =#+filetags:=, which is the 272 | correct way of setting file-wide tags in org-mode. 273 | (Thank you, Kaushal Modi!) 274 | - 2018-04-19 (v1.2.0): HTML5 275 | Org-static-blog now outputs valid HTML5 instead of XHTML. This makes 276 | the resulting HTML cleaner, but shouldn't impact your styles. Also, 277 | you can now customize your content language by setting 278 | =org-static-blog-langcode= and the HTML output has been fixed in a few 279 | places. 280 | (Thank you, Michael Cardell Widerkrantz!) 281 | - 2020-03-20 (v1.3.0): Nested directories, Translations, and more 282 | Improve handling of local variables (Thank you, Matthew Bauer) 283 | Rewrote README in org-mode (Thank you, Rafał -rsm- Marek) 284 | Adds support for localizations (Thank you, Rafał -rsm- Marek) 285 | Put license in a LICENSE file (Thank you, Jonas Bernoulli) 286 | Adds uption to force-rerender entire blog (Thank you, Winny) 287 | Support for non-flat directory structure (Thank you, Shmavon Gazanchyan) 288 | Support for "preview" slugs on index page (Thank you, K. Scarlet) 289 | Various bugfixes (Thank you, Matthew Bauer, luhuaei, neeasade, Yauhen Makei, Winny, zsxh) 290 | Translations in RU, BY, FR (Thank you, Yauhen Makei, Théo Jacquin) 291 | - 2020-07-20 (v1.4.0): 292 | Adds a command to create drafts (Thank you, Massimo Lauria) 293 | Adds optional RSS info (Thank you, Massimo Lauria) 294 | Restructures preamble and postamble to be more consistent (Thank you, Massimo Lauria) 295 | Translations in IT, ES (Thank you, Massimo Lauria, Alberto Álvarez) 296 | Option to make ellipsis link to full post (Thank you, jaor) 297 | Improves preview generation (Thank you, Allo) 298 | Render RSS dates as per RFT-822 and the RSS spec 299 | - 2021-03-05 (v1.5.0) 300 | Better awareness for posts in subdirectories (Thank you, Justin Abrahms) 301 | New custom variable org-static-blog-rss-max-entries (Thank you, jao) 302 | Can now exclude some posts from RSS feeds (Thank you, jao) 303 | New custom variable for index page header (Thank you, Bruno Deremble) 304 | - 2022-05-05 (v1.6.0) 305 | Adds ~#+description~ support that fills the description meta tag (Thank you, Guangwang Huang) 306 | Adds optional post slugs, and date before title (Thank you, jao) 307 | Correct date encoding in RSS and various RSS fixes (Thank you, Agnessa Bubowska) 308 | Ability to not publish subtrees by tag (Thank you, wangz) 309 | Fixes some warnings related to Emacs 28 (Thank you, Maciej Barć) 310 | - 2025-03-20 (v1.7.0) 311 | Fixes path related issues for drafts 312 | Optionally disables comments if a customizable tag is set 313 | Adds viewport metadata to header (Thank you, Aze) 314 | Various fixes to blog generation (Thank you, Aze) 315 | Makes whitespace cleaning of filename suggestions customizable (Thank you, Magnus Therning) 316 | Adds support for Open Graph meta tags (Thank you, Pablo González Carrizo) 317 | Various bugfixes (Thank you, Dou Meishi, Musa Al-hassy, Alexis Purslane, chenyo, Johnny5, Miao ZhiCheng) 318 | 319 | * LICENSE 320 | 321 | Copyright 2015, Bastian Bechtold 322 | 323 | Redistribution and use in source and binary forms, with or without 324 | modification, are permitted provided that the following conditions are 325 | met: 326 | 327 | 1. Redistributions of source code must retain the above copyright 328 | notice, this list of conditions and the following disclaimer. 329 | 330 | 2. Redistributions in binary form must reproduce the above copyright 331 | notice, this list of conditions and the following disclaimer in the 332 | documentation and/or other materials provided with the 333 | distribution. 334 | 335 | 3. Neither the name of the copyright holder nor the names of its 336 | contributors may be used to endorse or promote products derived 337 | from this software without specific prior written permission. 338 | 339 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 340 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 341 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 342 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 343 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 344 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 345 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 346 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 347 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 348 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 349 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 350 | 351 | -------------------------------------------------------------------------------- /org-static-blog.el: -------------------------------------------------------------------------------- 1 | ;;; org-static-blog.el --- a simple org-mode based static blog generator 2 | 3 | ;; Author: Bastian Bechtold 4 | ;; Contrib: Shmavon Gazanchyan, Rafał -rsm- Marek, neeasade, 5 | ;; Michael Cardell Widerkrantz, Matthew Bauer, Winny, Yauhen Makei, 6 | ;; luhuaei, zngguvnf, Qiantan Hong, Jonas Bernoulli, Théo Jacquin, 7 | ;; K. Scarlet, zsxh 8 | ;; URL: https://github.com/bastibe/org-static-blog 9 | ;; Version: 1.7.0 10 | ;; Package-Requires: ((emacs "24.3")) 11 | 12 | ;;; Commentary: 13 | 14 | ;; Static blog generators are a dime a dozen. This is one more, which 15 | ;; focuses on being simple. All files are simple org-mode files in a 16 | ;; directory. The only requirement is that every org file must have a 17 | ;; #+TITLE and a #+DATE, and optionally, #+FILETAGS. 18 | 19 | ;; This file is also available from marmalade and melpa-stable. 20 | 21 | ;; Set up your blog by customizing org-static-blog's parameters, then 22 | ;; call M-x org-static-blog-publish to render the whole blog or 23 | ;; M-x org-static-blog-publish-file filename.org to render only only 24 | ;; the file filename.org. 25 | 26 | ;; Above all, I tried to make org-static-blog as simple as possible. 27 | ;; There are no magic tricks, and all of the source code is meant to 28 | ;; be easy to read, understand and modify. 29 | 30 | ;; If you have questions, if you find bugs, or if you would like to 31 | ;; contribute something to org-static-blog, please open an issue or 32 | ;; pull request on Github. 33 | 34 | ;; Finally, I would like to remind you that I am developing this 35 | ;; project for free, and in my spare time. While I try to be as 36 | ;; accomodating as possible, I can not guarantee a timely response to 37 | ;; issues. Publishing Open Source Software on Github does not imply an 38 | ;; obligation to *fix your problem right now*. Please be civil. 39 | 40 | ;;; Code: 41 | 42 | (require 'cl-extra) 43 | (require 'org) 44 | (require 'ox-html) 45 | 46 | (defgroup org-static-blog nil 47 | "Settings for a static blog generator using org-mode" 48 | :version "1.6.0" 49 | :group 'applications) 50 | 51 | (defcustom org-static-blog-publish-url "https://example.com/" 52 | "URL of the blog." 53 | :type '(string) 54 | :safe t) 55 | 56 | (defcustom org-static-blog-publish-title "Example.com" 57 | "Title of the blog." 58 | :type '(string) 59 | :safe t) 60 | 61 | (defcustom org-static-blog-publish-directory "~/blog/" 62 | "Directory where published HTML files are stored." 63 | :type '(directory)) 64 | 65 | (defcustom org-static-blog-posts-directory "~/blog/posts/" 66 | "Directory where published ORG files are stored. 67 | When publishing, posts are rendered as HTML, and included in the 68 | index, archive, tags, and RSS feed." 69 | :type '(directory)) 70 | 71 | (defcustom org-static-blog-drafts-directory "~/blog/drafts/" 72 | "Directory where unpublished ORG files are stored. 73 | When publishing, draft are rendered as HTML, but not included in 74 | the index, archive, tags, or RSS feed." 75 | :type '(directory)) 76 | 77 | (defcustom org-static-blog-index-file "index.html" 78 | "File name of the blog landing page. 79 | The index page contains the most recent 80 | `org-static-blog-index-length` full-text posts." 81 | :type '(string) 82 | :safe t) 83 | 84 | (defcustom org-static-blog-index-length 5 85 | "Number of articles to include on index page." 86 | :type '(integer) 87 | :safe t) 88 | 89 | (defcustom org-static-blog-archive-file "archive.html" 90 | "File name of the list of all blog posts. 91 | The archive page lists all posts as headlines." 92 | :type '(string) 93 | :safe t) 94 | 95 | (defcustom org-static-blog-tags-file "tags.html" 96 | "File name of the list of all blog posts by tag. 97 | The tags page lists all posts as headlines." 98 | :type '(string) 99 | :safe t) 100 | 101 | (defcustom org-static-blog-enable-tags nil 102 | "Show tags below posts, and generate tag pages." 103 | :group 'org-static-blog 104 | :type '(boolean) 105 | :safe t) 106 | 107 | (defcustom org-static-blog-enable-deprecation-warning t 108 | "Show deprecation warnings." 109 | :type '(boolean)) 110 | 111 | (defcustom org-static-blog-rss-file "rss.xml" 112 | "File name of the RSS feed." 113 | :type '(string) 114 | :safe t) 115 | 116 | (defcustom org-static-blog-rss-excluded-tag nil 117 | "Posts with this tag won't be included in the RSS feeds." 118 | :type '(choice (const :tag "None" nil) 119 | (string :tag "Tag name")) 120 | :safe t) 121 | 122 | (defcustom org-static-blog-no-comments-tag nil 123 | "Posts with this tag won't include comments." 124 | :type '(choice (const :tag "None" nil) 125 | (string :tag "Tag name")) 126 | :safe t) 127 | 128 | (defcustom org-static-blog-rss-extra "" 129 | "Extra information for the RSS feed header. 130 | This information is placed right before the sequence of posts. 131 | You can add an icon for the feed, or advertise that you built 132 | your blog with emacs, org-mode and org-static-blog. 133 | " 134 | :type '(string) 135 | :safe t) 136 | 137 | (defcustom org-static-blog-rss-max-entries nil 138 | "Maximum number of entries in the RSS feed. 139 | If nil (the default), all existing posts are included." 140 | :type '(choice (const nil) integer) 141 | :safe t) 142 | 143 | (defcustom org-static-blog-enable-tag-rss nil 144 | "Whether to generate per tag RSS feeds. 145 | 146 | When this flag is set, an RSS file with name given by prefixing 147 | `org-static-blog-rss-file' with '-' is created for each 148 | existing tag. The options `org-static-blog-rss-extra', 149 | `org-static-blog-rss-max-entries' and 150 | `org-static-blog-rss-excluded-tag' are also used to construct 151 | per-tag RSS feeds." 152 | :type '(boolean)) 153 | 154 | (defcustom org-static-blog-page-header "" 155 | "HTML to put in the of each page." 156 | :type '(string) 157 | :safe t) 158 | 159 | (defcustom org-static-blog-page-preamble "" 160 | "HTML to put before the content of each page." 161 | :type '(string) 162 | :safe t) 163 | 164 | (defcustom org-static-blog-page-postamble "" 165 | "HTML to put after the content of each page." 166 | :type '(string) 167 | :safe t) 168 | 169 | (defcustom org-static-blog-index-front-matter "" 170 | "HTML to put at the beginning of the index page." 171 | :type '(string) 172 | :safe t) 173 | 174 | (defcustom org-static-blog-post-preamble-text "" 175 | "HTML to put before every post" 176 | :type '(string) 177 | :safe t) 178 | 179 | (defcustom org-static-blog-post-postamble-text "" 180 | "HTML to put before every post" 181 | :type '(string) 182 | :safe t) 183 | 184 | (defcustom org-static-blog-post-comments "" 185 | "HTML code for comments to put after each blog post." 186 | :type '(string) 187 | :safe t) 188 | 189 | (defcustom org-static-blog-langcode "en" 190 | "Language code for the blog content." 191 | :type '(string) 192 | :safe t) 193 | 194 | (defcustom org-static-blog-use-preview nil 195 | "Use preview versions of posts on multipost pages. 196 | 197 | See also `org-static-blog-preview-start', 198 | `org-static-blog-preview-end', `org-static-blog-preview-ellipsis' 199 | and `org-static-blog-preview-link-p'." 200 | :type '(boolean) 201 | :safe t) 202 | 203 | (defcustom org-static-blog-preview-start nil 204 | "Marker indicating the beginning of a post's preview. 205 | 206 | When set to nil, we look for the first occurence of

in the 207 | generated HTML. See also `org-static-blog-preview-end'." 208 | :type '(choice (const :tag "First paragraph" nil) (string)) 209 | :safe t) 210 | 211 | (defcustom org-static-blog-preview-end nil 212 | "Marker indicating the end of a post's preview. 213 | 214 | When set to nil, we look for the first occurence of

after 215 | `org-static-blog-preview-start' (or the first

if that is nil) 216 | in the generated HTML." 217 | :type '(choice (const :tag "First paragraph" nil) (string)) 218 | :safe t) 219 | 220 | (defcustom org-static-blog-preview-convert-titles t 221 | "When preview is enabled, convert

to

for the previews." 222 | :type '(boolean) 223 | :safe t) 224 | 225 | (defcustom org-static-blog-preview-ellipsis "(...)" 226 | "The HTML appended to the preview if some part of the post is hidden. 227 | 228 | The contents shown in the preview is determined by the values of 229 | the variables `org-static-blog-preview-start' and 230 | `org-static-blog-preview-end'." 231 | :type '(string) 232 | :safe t) 233 | 234 | (defcustom org-static-blog-no-post-tag "nonpost" 235 | "Do not pushlish the subtree with this tag or property." 236 | :type '(string) 237 | :safe t) 238 | 239 | (defcustom org-static-blog-preview-link-p nil 240 | "Whether to make the preview ellipsis a link to the article's page." 241 | :type '(boolean) 242 | :safe t) 243 | 244 | (defcustom org-static-blog-preview-date-first-p nil 245 | "If t, print post dates before title in the preview view." 246 | :type '(boolean) 247 | :safe t) 248 | 249 | (defcustom org-static-blog-suggested-filename-cleaning-regexp "\s" 250 | "Regexp used to clean the suggested filename." 251 | :type'(string) 252 | :safe t) 253 | 254 | (defcustom org-static-blog-enable-og-tags nil 255 | "Whether to generate Open Graph Protocol meta tags" 256 | :group 'org-static-blog 257 | :type '(boolean) 258 | :safe t) 259 | 260 | (defcustom org-static-blog-image "" 261 | "Default image relative url to be used as Open Graph image for posts. 262 | 263 | Only if og tags are enabled. It can be overridden with the 264 | `#+image` property in specfic posts." 265 | :type '(string) 266 | :safe t) 267 | 268 | ;; localization support 269 | (defconst org-static-blog-texts 270 | '((other-posts 271 | ("en" . "Other posts") 272 | ("pl" . "Pozostałe wpisy") 273 | ("ru" . "Другие публикации") 274 | ("by" . "Іншыя публікацыі") 275 | ("it" . "Altri articoli") 276 | ("es" . "Otros artículos") 277 | ("fr" . "Autres articles") 278 | ("zh" . "其他帖子") 279 | ("ja" . "他の投稿")) 280 | (date-format 281 | ("en" . "%d %b %Y") 282 | ("pl" . "%Y-%m-%d") 283 | ("ru" . "%d.%m.%Y") 284 | ("by" . "%d.%m.%Y") 285 | ("it" . "%d/%m/%Y") 286 | ("es" . "%d/%m/%Y") 287 | ("fr" . "%d-%m-%Y") 288 | ("zh" . "%Y-%m-%d") 289 | ("ja" . "%Y/%m/%d")) 290 | (tags 291 | ("en" . "Tags") 292 | ("pl" . "Tagi") 293 | ("ru" . "Ярлыки") 294 | ("by" . "Ярлыкі") 295 | ("it" . "Categorie") 296 | ("es" . "Categoría") 297 | ("fr" . "Tags") 298 | ("zh" . "标签") 299 | ("ja" . "タグ")) 300 | (archive 301 | ("en" . "Archive") 302 | ("pl" . "Archiwum") 303 | ("ru" . "Архив") 304 | ("by" . "Архіў") 305 | ("it" . "Archivio") 306 | ("es" . "Archivo") 307 | ("fr" . "Archive") 308 | ("zh" . "归档") 309 | ("ja" . "アーカイブ")) 310 | (posts-tagged 311 | ("en" . "Posts tagged") 312 | ("pl" . "Wpisy z tagiem") 313 | ("ru" . "Публикации с ярлыками") 314 | ("by" . "Публікацыі") 315 | ("it" . "Articoli nella categoria") 316 | ("es" . "Artículos de la categoría") 317 | ("fr" . "Articles tagués") 318 | ("zh" . "打标签的帖子") 319 | ("ja" . "タグ付けされた投稿")) 320 | (no-prev-post 321 | ("en" . "There is no previous post") 322 | ("pl" . "Poprzedni wpis nie istnieje") 323 | ("ru" . "Нет предыдущей публикации") 324 | ("by" . "Няма папярэдняй публікацыі") 325 | ("it" . "Non c'è nessun articolo precedente") 326 | ("es" . "No existe un artículo precedente") 327 | ("fr" . "Il n'y a pas d'article précédent") 328 | ("zh" . "无更旧的帖子") 329 | ("ja" . "前の投稿はありません")) 330 | (no-next-post 331 | ("en" . "There is no next post") 332 | ("pl" . "Następny wpis nie istnieje") 333 | ("ru" . "Нет следующей публикации") 334 | ("by" . "Няма наступнай публікацыі") 335 | ("it" . "Non c'è nessun articolo successivo") 336 | ("es" . "No hay artículo siguiente") 337 | ("fr" . "Il n'y a pas d'article suivants") 338 | ("zh" . "无更新的帖子") 339 | ("ja" . "次の投稿はありません")) 340 | (title 341 | ("en" . "Title: ") 342 | ("pl" . "Tytuł: ") 343 | ("ru" . "Заголовок: ") 344 | ("by" . "Загаловак: ") 345 | ("it" . "Titolo: ") 346 | ("es" . "Título: ") 347 | ("fr" . "Titre : ") 348 | ("zh" . "标题:") 349 | ("ja" . "タイトル: ")) 350 | (filename 351 | ("en" . "Filename: ") 352 | ("pl" . "Nazwa pliku: ") 353 | ("ru" . "Имя файла: ") 354 | ("by" . "Імя файла: ") 355 | ("it" . "Nome del file: ") 356 | ("es" . "Nombre del archivo: ") 357 | ("fr" . "Nom du fichier :") 358 | ("zh" . "文件名:") 359 | ("ja" . "ファイル名: ")))) 360 | 361 | (defun concat-to-dir (dir filename) 362 | "Concat filename to another path interpreted as a directory." 363 | (concat (file-name-as-directory dir) filename)) 364 | 365 | (defun org-static-blog-template (tTitle tContent &optional tDescription tImage tUrl) 366 | "Create the template that is used to generate the static pages." 367 | (concat 368 | "\n" 369 | "\n" 370 | "\n" 371 | "\n" 372 | (when tDescription 373 | (format "\n" tDescription)) 374 | "\n" 378 | "" tTitle "\n" 379 | 380 | (when org-static-blog-enable-og-tags 381 | (concat 382 | "\n" 383 | "\n" 384 | (when tDescription 385 | (format "\n" tDescription)) 386 | (when tUrl 387 | (format "\n" tUrl)) 388 | (if tImage 389 | (format "\n" 390 | (org-static-blog-get-absolute-url tImage)) 391 | (when (> (length org-static-blog-image) 0) 392 | (format "\n" 393 | (org-static-blog-get-absolute-url org-static-blog-image)))))) 394 | org-static-blog-page-header 395 | "\n" 396 | "\n" 397 | "
" 398 | org-static-blog-page-preamble 399 | "
\n" 400 | "
\n" 401 | tContent 402 | "
\n" 403 | "
" 404 | org-static-blog-page-postamble 405 | "
\n" 406 | "\n" 407 | "\n")) 408 | 409 | (defun org-static-blog-gettext (text-id) 410 | "Return localized text. 411 | Depends on org-static-blog-langcode and org-static-blog-texts." 412 | (let* ((text-node (assoc text-id org-static-blog-texts)) 413 | (text-lang-node (if text-node 414 | (assoc org-static-blog-langcode text-node) 415 | nil))) 416 | (if text-lang-node 417 | (cdr text-lang-node) 418 | (concat "[" (symbol-name text-id) ":" org-static-blog-langcode "]")))) 419 | 420 | 421 | ;;;###autoload 422 | (defun org-static-blog-publish (&optional force-render) 423 | "Render all blog posts, the index, archive, tags, and RSS feed. 424 | Only blog posts that changed since the HTML was created are 425 | re-rendered. 426 | 427 | With a prefix argument, all blog posts are re-rendered 428 | unconditionally." 429 | (interactive "P") 430 | (dolist (file (append (org-static-blog-get-post-filenames) 431 | (org-static-blog-get-draft-filenames))) 432 | (when (or force-render (org-static-blog-needs-publishing-p file)) 433 | (org-static-blog-publish-file file))) 434 | ;; don't spam too many deprecation warnings: 435 | (let ((org-static-blog-enable-deprecation-warning nil)) 436 | (org-static-blog-assemble-index) 437 | (org-static-blog-assemble-rss) 438 | (org-static-blog-assemble-archive) 439 | (if org-static-blog-enable-tags 440 | (org-static-blog-assemble-tags)))) 441 | 442 | (defun org-static-blog-needs-publishing-p (post-filename) 443 | "Check whether POST-FILENAME was changed since last render." 444 | (let ((pub-filename 445 | (org-static-blog-matching-publish-filename post-filename))) 446 | (not (and (file-exists-p pub-filename) 447 | (file-newer-than-file-p pub-filename post-filename))))) 448 | 449 | (defun org-static-blog-matching-publish-filename (post-filename) 450 | "Generate HTML file name for POST-FILENAME." 451 | (concat-to-dir org-static-blog-publish-directory 452 | (org-static-blog-get-post-public-path post-filename))) 453 | 454 | (defun org-static-blog-get-post-filenames () 455 | "Returns a list of all posts." 456 | (directory-files-recursively 457 | org-static-blog-posts-directory ".*\\.org$")) 458 | 459 | (defun org-static-blog-get-draft-filenames () 460 | "Returns a list of all drafts." 461 | (directory-files-recursively 462 | org-static-blog-drafts-directory ".*\\.org$")) 463 | 464 | (defun org-static-blog-file-buffer (file) 465 | "Return the buffer open with a full filepath, or nil." 466 | (require 'seq) 467 | (make-directory (file-name-directory file) t) 468 | (car (seq-filter 469 | (lambda (buf) 470 | (string= (with-current-buffer buf buffer-file-name) file)) 471 | (buffer-list)))) 472 | 473 | ;; This macro is needed for many of the following functions. 474 | (defmacro org-static-blog-with-find-file (file contents &rest body) 475 | "Executes BODY in FILE. Use this to insert text into FILE. 476 | The buffer is disposed after the macro exits (unless it already 477 | existed before)." 478 | `(save-excursion 479 | (let ((current-buffer (current-buffer)) 480 | (buffer-exists (org-static-blog-file-buffer ,file)) 481 | (result nil) 482 | (auto-insert nil) 483 | (contents ,contents)) 484 | (if buffer-exists 485 | (switch-to-buffer buffer-exists) 486 | (find-file ,file)) 487 | (erase-buffer) 488 | (insert contents) 489 | (setq result (progn ,@body)) 490 | (basic-save-buffer) 491 | (unless buffer-exists 492 | (kill-buffer)) 493 | (switch-to-buffer current-buffer) 494 | result))) 495 | 496 | (defun org-static-blog-get-date (post-filename) 497 | "Extract the `#+date:` from POST-FILENAME as date-time." 498 | (let ((case-fold-search t)) 499 | (with-temp-buffer 500 | (insert-file-contents post-filename) 501 | (goto-char (point-min)) 502 | (if (search-forward-regexp "^\\#\\+date:[ ]*[[<]?\\([^]>]+\\)[]>]?$" nil t) 503 | (date-to-time (match-string 1)) 504 | (time-since 0))))) 505 | 506 | (defun org-static-blog-get-title (post-filename) 507 | "Extract the `#+title:` from POST-FILENAME." 508 | (let ((case-fold-search t)) 509 | (with-temp-buffer 510 | (insert-file-contents post-filename) 511 | (goto-char (point-min)) 512 | (if (search-forward-regexp "^\\#\\+title:[ ]*\\(.+\\)$" nil t) 513 | (match-string 1) 514 | (warn "%s file does not have a title, using %s as the title" post-filename post-filename) 515 | post-filename)))) 516 | 517 | (defun org-static-blog-get-description (post-filename) 518 | "Extract the `#+description:` from POST-FILENAME." 519 | (let ((case-fold-search t)) 520 | (with-temp-buffer 521 | (insert-file-contents post-filename) 522 | (goto-char (point-min)) 523 | (when (search-forward-regexp "^\\#\\+description:[ ]*\\(.+\\)$" nil t) 524 | (let ((description (string-trim (match-string 1)))) 525 | (unless (zerop (length description)) 526 | description)))))) 527 | 528 | (defun org-static-blog-get-image (post-filename) 529 | "Extract the `#+image:` from POST-FILENAME." 530 | (let ((case-fold-search t)) 531 | (with-temp-buffer 532 | (insert-file-contents post-filename) 533 | (goto-char (point-min)) 534 | (when (search-forward-regexp "^\\#\\+image:[ ]*\\(.+\\)$" nil t) 535 | (let ((image (string-trim (match-string 1)))) 536 | (unless (zerop (length image)) 537 | image)))))) 538 | 539 | (defun org-static-blog-get-tags (post-filename) 540 | "Extract the `#+filetags:` from POST-FILENAME as list of strings." 541 | (let ((case-fold-search t)) 542 | (with-temp-buffer 543 | (insert-file-contents post-filename) 544 | (goto-char (point-min)) 545 | (if (search-forward-regexp "^\\#\\+filetags:[ ]*:\\(.*\\):$" nil t) 546 | (split-string (match-string 1) ":") 547 | (if (search-forward-regexp "^\\#\\+filetags:[ ]*\\(.+\\)$" nil t) 548 | (split-string (match-string 1)) 549 | ))))) 550 | 551 | (defun org-static-blog-get-tag-tree () 552 | "Return an association list of tags to filenames. 553 | e.g. `(('foo' 'file1.org' 'file2.org') ('bar' 'file2.org'))`" 554 | (let ((tag-tree '())) 555 | (dolist (post-filename (org-static-blog-get-post-filenames)) 556 | (let ((tags (org-static-blog-get-tags post-filename))) 557 | (dolist (tag (remove org-static-blog-rss-excluded-tag tags)) 558 | (if (assoc-string tag tag-tree t) 559 | (push post-filename (cdr (assoc-string tag tag-tree t))) 560 | (push (cons tag (list post-filename)) tag-tree))))) 561 | tag-tree)) 562 | 563 | (defun org-static-blog--preview-region () 564 | "Find the start and end of the preview in the current buffer." 565 | (goto-char (point-min)) 566 | (if org-static-blog-preview-end 567 | (when (or (search-forward (or org-static-blog-preview-start "

") nil t) 568 | (search-forward "

" nil t)) 569 | (let ((start (match-beginning 0))) 570 | (or (search-forward org-static-blog-preview-end nil t) 571 | (search-forward "

" nil t)) 572 | (buffer-substring-no-properties start (point)))) 573 | (when (search-forward (or org-static-blog-preview-start "

") nil t) 574 | (let ((start (match-beginning 0))) 575 | (search-forward "

") 576 | (buffer-substring-no-properties start (point)))))) 577 | 578 | (defun org-static-blog-get-preview (post-filename) 579 | "Get title, date, tags from POST-FILENAME and get the first paragraph from the rendered HTML. 580 | If the HTML body contains multiple paragraphs, include only the first paragraph, 581 | and display an ellipsis. 582 | Preamble and Postamble are excluded, too." 583 | (with-temp-buffer 584 | (insert-file-contents (org-static-blog-matching-publish-filename post-filename)) 585 | (let ((post-title (org-static-blog-get-title post-filename)) 586 | (post-date (org-static-blog-get-date post-filename)) 587 | (post-taglist (org-static-blog-post-taglist post-filename)) 588 | (post-ellipsis "") 589 | (preview-region (org-static-blog--preview-region))) 590 | (when (and preview-region (search-forward "

" nil t)) 591 | (setq post-ellipsis 592 | (concat (when org-static-blog-preview-link-p 593 | (format "" 594 | (org-static-blog-get-post-url post-filename))) 595 | org-static-blog-preview-ellipsis 596 | (when org-static-blog-preview-link-p "\n")))) 597 | ;; Put the substrings together. 598 | (let ((title-link 599 | (format "

%s

" 600 | (org-static-blog-get-post-url post-filename) post-title)) 601 | (date-link 602 | (format-time-string (concat "
" 603 | (org-static-blog-gettext 'date-format) 604 | "
") 605 | post-date))) 606 | (concat 607 | (if org-static-blog-preview-date-first-p 608 | (concat date-link title-link) 609 | (concat title-link date-link)) 610 | preview-region 611 | post-ellipsis 612 | (format "
%s
" post-taglist)))))) 613 | 614 | 615 | (defun org-static-blog-get-post-content (post-filename &optional exclude-title) 616 | "Get the rendered HTML body without headers from POST-FILENAME. 617 | Preamble and Postamble are excluded, too." 618 | ;; NB! The following code assumes the post is using default template. 619 | ;; See: org-static-blog-publish-file 620 | (let ((publish-filename (org-static-blog-matching-publish-filename post-filename))) 621 | (with-temp-buffer 622 | (insert-file-contents publish-filename) 623 | (buffer-substring-no-properties 624 | (progn 625 | (goto-char (point-min)) 626 | (if exclude-title 627 | (progn (search-forward "

") 628 | (search-forward "

")) 629 | (search-forward "
")) 630 | (point)) 631 | (progn 632 | (goto-char (point-max)) 633 | ;; Search backward for the post content (by org-static-blog-render-post-content). 634 | ;; See: org-static-blog-template 635 | (search-backward "
") 636 | (search-backward "
") 637 | ;; If comments section exists, it is then one div backward. 638 | ;; See: org-static-blog-post-postamble 639 | (search-backward "
" nil t) 640 | (point)))))) 641 | 642 | (defun org-static-blog-get-absolute-url (relative-url) 643 | "Returns absolute URL based on the RELATIVE-URL passed to the function. 644 | 645 | For example, when `org-static-blog-publish-url` is set to 'https://example.com/' 646 | and `relative-url` is passed as 'archive.html' then the function 647 | will return 'https://example.com/archive.html'." 648 | (concat-to-dir org-static-blog-publish-url relative-url)) 649 | 650 | (defun org-static-blog-get-post-url (post-filename) 651 | "Returns absolute URL to the published POST-FILENAME. 652 | 653 | This function concatenates publish URL and generated custom filepath to the 654 | published HTML version of the post." 655 | (org-static-blog-get-absolute-url 656 | (org-static-blog-get-post-public-path post-filename))) 657 | 658 | (defun org-static-blog-get-post-public-path (post-filename) 659 | "Returns post filepath in public directory. 660 | This function retrieves relative path to the post file in posts 661 | or drafts directories to published HTML version of the post." 662 | (let* ((true-post-filename (file-truename post-filename)) 663 | (true-posts-directory (file-truename org-static-blog-posts-directory)) 664 | (root-dir (if (string-prefix-p true-posts-directory true-post-filename) 665 | true-posts-directory 666 | (file-truename org-static-blog-drafts-directory)))) 667 | (concat (file-name-sans-extension (file-relative-name true-post-filename root-dir)) ".html"))) 668 | 669 | (defun org-static-blog-get-relative-path (post-filename) 670 | "Removes absolute directory path from POST-FILENAME and changes file extention 671 | from `.org` to `.html`. Returns filepath to HTML file relative to posts or drafts directories. 672 | 673 | Works with both posts and drafts directories. 674 | 675 | For example, when `org-static-blog-posts-directory` is set to '~/blog/posts' 676 | and `post-filename` is passed as '~/blog/posts/my-life-update.org' then the function 677 | will return 'my-life-update.html'." 678 | (concat (file-name-sans-extension (file-relative-name post-filename org-static-blog-posts-directory)) 679 | ".html")) 680 | 681 | ;;;###autoload 682 | (defun org-static-blog-publish-file (post-filename) 683 | "Publish a single POST-FILENAME. 684 | The index, archive, tags, and RSS feed are not updated." 685 | (interactive "f") 686 | (org-static-blog-with-find-file 687 | (org-static-blog-matching-publish-filename post-filename) 688 | (org-static-blog-template 689 | (org-static-blog-get-title post-filename) 690 | (concat 691 | (org-static-blog-post-preamble post-filename) 692 | (org-static-blog-render-post-content post-filename) 693 | (org-static-blog-post-postamble post-filename)) 694 | (org-static-blog-get-description post-filename) 695 | (org-static-blog-get-image post-filename) 696 | (org-static-blog-get-post-url post-filename)))) 697 | 698 | 699 | (defun org-static-blog-render-post-content (post-filename) 700 | "Render blog content as bare HTML without header." 701 | (let ((org-html-doctype "html5") 702 | (org-html-html5-fancy t)) 703 | (save-excursion 704 | (let ((current-buffer (current-buffer)) 705 | (buffer-exists (org-static-blog-file-buffer post-filename)) 706 | (result nil)) 707 | (with-temp-buffer 708 | (if buffer-exists 709 | (insert-buffer-substring buffer-exists) 710 | (insert-file-contents post-filename)) 711 | (org-mode) 712 | (goto-char (point-min)) 713 | (org-map-entries 714 | (lambda () 715 | (setq org-map-continue-from (point)) 716 | (org-cut-subtree)) 717 | org-static-blog-no-post-tag) 718 | (setq result 719 | (org-export-as 'org-static-blog-post-bare nil nil nil nil)) 720 | (switch-to-buffer current-buffer) 721 | result))))) 722 | 723 | (org-export-define-derived-backend 'org-static-blog-post-bare 'html 724 | :translate-alist '((template . (lambda (contents info) contents)))) 725 | 726 | (defun org-static-blog-assemble-index () 727 | "Assemble the blog index page. 728 | The index page contains the last `org-static-blog-index-length` 729 | posts as full text posts." 730 | (let ((post-filenames (org-static-blog-get-post-filenames))) 731 | ;; reverse-sort, so that the later `last` will grab the newest posts 732 | (setq post-filenames (sort post-filenames (lambda (x y) (time-less-p (org-static-blog-get-date x) 733 | (org-static-blog-get-date y))))) 734 | (org-static-blog-assemble-multipost-page 735 | (concat-to-dir org-static-blog-publish-directory org-static-blog-index-file) 736 | (last post-filenames org-static-blog-index-length) 737 | org-static-blog-index-front-matter))) 738 | 739 | (defun org-static-blog-assemble-multipost-page (pub-filename post-filenames &optional front-matter) 740 | "Assemble a page that contains multiple posts one after another. 741 | Posts are sorted in descending time." 742 | (setq post-filenames (sort post-filenames (lambda (x y) (time-less-p (org-static-blog-get-date y) 743 | (org-static-blog-get-date x))))) 744 | (org-static-blog-with-find-file 745 | pub-filename 746 | (org-static-blog-template 747 | org-static-blog-publish-title 748 | (concat 749 | (when front-matter front-matter) 750 | (apply 'concat (mapcar 751 | (if org-static-blog-use-preview 752 | 'org-static-blog-get-preview 753 | 'org-static-blog-get-post-content) post-filenames)) 754 | "\n")))) 757 | 758 | 759 | 760 | (defun org-static-blog-post-preamble (post-filename) 761 | "Returns the formatted date and headline of the post. 762 | This function is called for every post and prepended to the post body. 763 | Modify this function if you want to change a posts headline." 764 | (concat 765 | org-static-blog-post-preamble-text 766 | "
" (format-time-string (org-static-blog-gettext 'date-format) 767 | (org-static-blog-get-date post-filename)) 768 | "
" 769 | "

" 770 | "" (org-static-blog-get-title post-filename) "" 771 | "

\n")) 772 | 773 | 774 | (defun org-static-blog-post-taglist (post-filename) 775 | "Returns the tag list of the post. 776 | This part will be attached at the end of the post, after 777 | the taglist, in a
...
block." 778 | (let ((taglist-content "") 779 | (tags (remove org-static-blog-no-comments-tag 780 | (remove org-static-blog-rss-excluded-tag 781 | (org-static-blog-get-tags post-filename))))) 782 | (when (and tags org-static-blog-enable-tags) 783 | (setq taglist-content (concat "" (org-static-blog-gettext 'tags) ": ")) 786 | (dolist (tag tags) 787 | (setq taglist-content (concat taglist-content "" tag " ")))) 790 | taglist-content)) 791 | 792 | (defun org-static-blog-post-postamble (post-filename) 793 | "Returns the tag list and comment box at the end of a post. 794 | This function is called for every post and the returned string is 795 | appended to the post body, and includes the tag list generated by 796 | followed by the HTML code for comments." 797 | (concat "
" 798 | (org-static-blog-post-taglist post-filename) 799 | "
" 800 | (if (or (string= org-static-blog-post-comments "") 801 | (member org-static-blog-no-comments-tag (org-static-blog-get-tags post-filename))) 802 | "" 803 | (concat "\n
" 804 | org-static-blog-post-comments 805 | "
")) 806 | org-static-blog-post-postamble-text)) 807 | 808 | 809 | (defun org-static-blog--prune-items (items) 810 | "Limit, if needed, the items to be included in an RSS feed." 811 | (if (and org-static-blog-rss-max-entries (> org-static-blog-rss-max-entries 0)) 812 | (let ((excess (- (length items) org-static-blog-rss-max-entries))) 813 | (if (> excess 0) (butlast items excess) items)) 814 | items)) 815 | 816 | (defun org-static-blog--rss-filename (&optional tag) 817 | "Full path to the RSS file for the given TAG." 818 | (concat-to-dir org-static-blog-publish-directory 819 | (concat tag (when tag "-") org-static-blog-rss-file))) 820 | 821 | (defun org-static-blog--write-rss (items &optional tag) 822 | "Generates an RSS file for the given TAG, or for all tags is TAG is nil." 823 | (let ((title (format "%s%s" 824 | org-static-blog-publish-title 825 | (if tag (concat " - " tag) ""))) 826 | (url (format "%s%s" 827 | org-static-blog-publish-url 828 | (if tag (concat "/tag-" (downcase tag) ".html") ""))) 829 | (items (sort items (lambda (x y) (time-less-p (car y) (car x)))))) 830 | (org-static-blog-with-find-file 831 | (org-static-blog--rss-filename tag) 832 | (concat "\n" 833 | "\n" 834 | "\n" 835 | "<![CDATA[" title "]]>\n" 836 | "\n" 837 | "" url "\n" 838 | "" (let ((system-time-locale "C")) ; force dates to render as per RSS spec 839 | (format-time-string "%a, %d %b %Y %H:%M:%S %z" (current-time))) 840 | "\n" 841 | org-static-blog-rss-extra 842 | (apply 'concat (mapcar 'cdr (org-static-blog--prune-items items))) 843 | "\n" 844 | "\n")))) 845 | 846 | (defun org-static-blog-assemble-rss () 847 | "Assemble the blog RSS feed. 848 | The RSS-feed is an XML file that contains every blog post in a 849 | machine-readable format." 850 | (let ((system-time-locale "en_US.utf-8") ; force dates to render as per RSS spec 851 | (rss-items nil) 852 | (rss-tag-items nil)) 853 | (dolist (post-filename (org-static-blog-get-post-filenames)) 854 | (let ((rss-date (org-static-blog-get-date post-filename)) 855 | (rss-text (org-static-blog-get-rss-item post-filename)) 856 | (tags (org-static-blog-get-tags post-filename))) 857 | (when (or (null org-static-blog-rss-excluded-tag) 858 | (not (member org-static-blog-rss-excluded-tag tags))) 859 | (let ((item (cons rss-date rss-text))) 860 | (add-to-list 'rss-items item) 861 | (when org-static-blog-enable-tag-rss 862 | (dolist (tag tags) 863 | (let ((items (cons item (cdr (assoc tag rss-tag-items))))) 864 | (setf (alist-get tag rss-tag-items nil t 'string=) items)))))))) 865 | (org-static-blog--write-rss rss-items) 866 | (message "%s" rss-tag-items) 867 | (dolist (x rss-tag-items) (org-static-blog--write-rss (cdr x) (car x))))) 868 | 869 | (defun org-static-blog-get-rss-item (post-filename) 870 | "Assemble RSS item from post-filename. 871 | The HTML content is taken from the rendered HTML post." 872 | (concat 873 | "\n" 874 | " <![CDATA[" (org-static-blog-get-title post-filename) "]]>\n" 875 | " \n" 878 | (let ((categories "")) 879 | (when (and (org-static-blog-get-tags post-filename) org-static-blog-enable-tags) 880 | (dolist (tag (org-static-blog-get-tags post-filename)) 881 | (setq categories (concat categories 882 | " \n")))) 883 | categories) 884 | " " 885 | (url-encode-url (org-static-blog-get-post-url post-filename)) 886 | "\n" 887 | " " 888 | (url-encode-url (org-static-blog-get-post-url post-filename)) 889 | "\n" 890 | " " 891 | (let ((system-time-locale "C")) ; force dates to render as per RSS spec 892 | (format-time-string "%a, %d %b %Y %H:%M:%S %z" (org-static-blog-get-date post-filename))) 893 | "\n" 894 | "\n")) 895 | 896 | (defun org-static-blog-assemble-archive () 897 | "Re-render the blog archive page. 898 | The archive page contains single-line links and dates for every 899 | blog post, but no post body." 900 | (let ((archive-filename (concat-to-dir org-static-blog-publish-directory org-static-blog-archive-file)) 901 | (archive-entries nil) 902 | (post-filenames (org-static-blog-get-post-filenames))) 903 | (setq post-filenames (sort post-filenames (lambda (x y) (time-less-p 904 | (org-static-blog-get-date y) 905 | (org-static-blog-get-date x))))) 906 | (org-static-blog-with-find-file 907 | archive-filename 908 | (org-static-blog-template 909 | org-static-blog-publish-title 910 | (concat 911 | "

" (org-static-blog-gettext 'archive) "

\n" 912 | (apply 'concat (mapcar 'org-static-blog-get-post-summary post-filenames))))))) 913 | 914 | (defun org-static-blog-get-post-summary (post-filename) 915 | "Assemble post summary for an archive page. 916 | This function is called for every post on the archive and 917 | tags-archive page. Modify this function if you want to change an 918 | archive headline." 919 | (concat 920 | "
" 921 | (format-time-string (org-static-blog-gettext 'date-format) (org-static-blog-get-date post-filename)) 922 | "
" 923 | "

" 924 | "" (org-static-blog-get-title post-filename) "" 925 | "

\n")) 926 | 927 | (defun org-static-blog-assemble-tags () 928 | "Render the tag archive and tag pages." 929 | (org-static-blog-assemble-tags-archive) 930 | (dolist (tag (org-static-blog-get-tag-tree)) 931 | (org-static-blog-assemble-multipost-page 932 | (concat-to-dir org-static-blog-publish-directory 933 | (concat "tag-" (downcase (car tag)) ".html")) 934 | (cdr tag) 935 | (concat "

" (org-static-blog-gettext 'posts-tagged) " \"" (car tag) "\":

")))) 936 | 937 | (defun org-static-blog-assemble-tags-archive-tag (tag) 938 | "Assemble single TAG for all filenames." 939 | (let ((post-filenames (cdr tag))) 940 | (setq post-filenames 941 | (sort post-filenames (lambda (x y) (time-less-p (org-static-blog-get-date x) 942 | (org-static-blog-get-date y))))) 943 | (concat "

" (org-static-blog-gettext 'posts-tagged) " \"" (downcase (car tag)) "\":

\n" 944 | (apply 'concat (mapcar 'org-static-blog-get-post-summary post-filenames))))) 945 | 946 | (defun org-static-blog-assemble-tags-archive () 947 | "Assemble the blog tag archive page. 948 | The archive page contains single-line links and dates for every 949 | blog post, sorted by tags, but no post body." 950 | (let ((tags-archive-filename (concat-to-dir org-static-blog-publish-directory org-static-blog-tags-file)) 951 | (tag-tree (org-static-blog-get-tag-tree))) 952 | (setq tag-tree (sort tag-tree (lambda (x y) (string-greaterp (car y) (car x))))) 953 | (org-static-blog-with-find-file 954 | tags-archive-filename 955 | (org-static-blog-template 956 | org-static-blog-publish-title 957 | (concat 958 | "

" (org-static-blog-gettext 'tags) "

\n" 959 | (apply 'concat (mapcar 'org-static-blog-assemble-tags-archive-tag tag-tree))))))) 960 | 961 | (defun org-static-blog-open-previous-post () 962 | "Opens previous blog post." 963 | (interactive) 964 | (let ((posts (sort (org-static-blog-get-post-filenames) 965 | (lambda (x y) 966 | (time-less-p (org-static-blog-get-date y) 967 | (org-static-blog-get-date x))))) 968 | (current-post (buffer-file-name))) 969 | (while (and posts 970 | (not (string-equal 971 | (file-name-nondirectory current-post) 972 | (file-name-nondirectory (car posts))))) 973 | (setq posts (cdr posts))) 974 | (if (> (cl-list-length posts) 1) 975 | (find-file (cadr posts)) 976 | (message (org-static-blog-gettext 'no-prev-post))))) 977 | 978 | (defun org-static-blog-open-next-post () 979 | "Opens next blog post." 980 | (interactive) 981 | (let ((posts (sort (org-static-blog-get-post-filenames) 982 | (lambda (x y) 983 | (time-less-p (org-static-blog-get-date x) 984 | (org-static-blog-get-date y))))) 985 | (current-post (buffer-file-name))) 986 | (while (and posts 987 | (not (string-equal 988 | (file-name-nondirectory current-post) 989 | (file-name-nondirectory (car posts))))) 990 | (setq posts (cdr posts))) 991 | (if (> (cl-list-length posts) 1) 992 | (find-file (cadr posts)) 993 | (message (org-static-blog-gettext 'no-next-post))))) 994 | 995 | (defun org-static-blog-open-matching-publish-file () 996 | "Opens HTML for post." 997 | (interactive) 998 | (find-file (org-static-blog-matching-publish-filename (buffer-file-name)))) 999 | 1000 | ;;;###autoload 1001 | (defun org-static-blog-create-new-post (&optional draft) 1002 | "Creates a new blog post. 1003 | Prompts for a title and proposes a file name. The file name is 1004 | only a suggestion; You can choose any other file name if you so 1005 | choose." 1006 | (interactive) 1007 | (let ((title (read-string (org-static-blog-gettext 'title)))) 1008 | (find-file (concat-to-dir 1009 | (if draft 1010 | org-static-blog-drafts-directory 1011 | org-static-blog-posts-directory) 1012 | (read-string (org-static-blog-gettext 'filename) 1013 | (concat (format-time-string "%Y-%m-%d-" (current-time)) 1014 | (replace-regexp-in-string org-static-blog-suggested-filename-cleaning-regexp 1015 | "-" (downcase title)) 1016 | ".org")))) 1017 | (insert "#+title: " title "\n" 1018 | "#+date: " (format-time-string "<%Y-%m-%d %H:%M>") "\n" 1019 | "#+description: \n" 1020 | "#+filetags: "))) 1021 | 1022 | ;;;###autoload 1023 | (defun org-static-blog-create-new-draft () 1024 | "Creates a new blog draft. 1025 | Prompts for a title and proposes a file name. The file name is 1026 | only a suggestion; You can choose any other file name if you so 1027 | choose." 1028 | (interactive) 1029 | (org-static-blog-create-new-post 't)) 1030 | 1031 | 1032 | ;;;###autoload 1033 | (define-derived-mode org-static-blog-mode org-mode "OSB" 1034 | "Blogging with org-mode and emacs." 1035 | (run-mode-hooks) 1036 | :group 'org-static-blog) 1037 | 1038 | ;; Key bindings 1039 | (define-key org-static-blog-mode-map (kbd "C-c C-f") 'org-static-blog-open-next-post) 1040 | (define-key org-static-blog-mode-map (kbd "C-c C-b") 'org-static-blog-open-previous-post) 1041 | (define-key org-static-blog-mode-map (kbd "C-c C-p") 'org-static-blog-open-matching-publish-file) 1042 | (define-key org-static-blog-mode-map (kbd "C-c C-n") 'org-static-blog-create-new-post) 1043 | (define-key org-static-blog-mode-map (kbd "C-c C-d") 'org-static-blog-create-new-draft) 1044 | 1045 | (provide 'org-static-blog) 1046 | 1047 | ;;; org-static-blog.el ends here 1048 | --------------------------------------------------------------------------------