Leanpub has a robust web interface, but it seemed appropriate to create "Writing PHP with Emacs" using Emacs itself. This is how I do it, from managing ideas to publishing the content itself.

Managing ideas

Before I started writing the book I had a single text file that I collected ideas in. This worked okay in the beginning, but once things started moving along it became unwieldy to manage.

I switched to keeping my notes in zetteldeft which made life much easier. There is a single note titled "Writing PHP with Emacs" which I use as an entry point. All related notes are linked to this one, and I also tag them with #php-with-emacs so I can quickly search for them.

Not all of these notes are directly related to the book, but they might contain information that I want to include in the future.


The entire book plan is stored in a single TODO.org file in the project directory. I use it to keep a list of the overall contents, as well as planning out individual chapters.

The plan looks a little like this (with content removed for brevity):

#+TITLE: PHP with Emacs - TODO List

* TODO Milestones [4/5]
** COMPLETE Version 0.2
   CLOSED: [2020-06-09 Tue 19:55] DEADLINE: <2020-05-31 Sun>
*** COMPLETE php-mode: Customization - Using TAGS files [3/3]
    CLOSED: [2020-05-30 Sat 10:26] SCHEDULED: <2020-05-28 Thu>
    :EFFORT:   1:00
    Prefer to use php-lsp, but this is an option too.
     - [X] What TAGS files can be used for
     - [X] Generating
     - [X] Navigating using a TAGS file
       Shortcuts and everything go here.
* TODO Backlog [1/22]
** TODO Configuring a project for WordPress
** TODO Configuring a project for Drupal

I use org-columns for quickly viewing the list of milestones. From this view I can set estimates and view time worked on each milestone and sub-task.

My book plan using column mode

The Backlog heading keeps a list of all future content that I want to work on. When it's time to move an item to the plan, I'll refile it to a milestone using a custom function called pn/org-refile-in-file:

(defvar pn/org-last-refile-marker nil "Marker for last refile")

(defun pn/org-refile-in-file (&optional prefix)
  "Refile to a target within the current file."
  (let ((helm-org-headings-actions
	 '(("Refile to this heading" . helm-org--refile-heading-to))))
      (org-end-of-subtree t)
      (setq pn/org-last-refile-marker (point-marker)))))

I use this instead of org-refile as I have a lot of org files that I don't want to be used as targets. Once a headline has been moved to a milestone I'll add a time estimate and schedule it. I use org-projectile so any scheduled tasks in the book TODO show up in my agenda.

Organizing files

The project directory contains the manuscript directory, which is used by Leanpub to generate the book. It also contains a lightweight Emacs user directory which I use when generating screenshots.

I use binder to keep a list of manuscript files. binder also supports adding file-specific notes, although I prefer to keep things in my TODO file.

Using binder to view the book

For most navigation I'll use projectile, but binder is helpful when trying to get an overall look at the book or when I'm jumping between chapters.


Leanpub has its own offshoot of Markdown called Markua, so everything is written and edited using markdown-mode+. I would have preferred to write in org-mode - and there is an ox-leanpub exporter - but I didn't want to add an extra step to the write -> publish process. Maybe next time.

One of the things that caught me out at the start was line breaks. Markdown will normally ignore a single line break, so hard-wrapped lines will still be treated as a single line. Markua treats them as hard line breaks, so it altered the look of the book and made pages too narrow. I switched to visual-line-mode which wraps lines visually but doesn't alter the text.

I use a .dir-locals.el file to automatically enable specific modes and to increase the font size.

((markdown-mode . ((eval . (progn (turn-off-auto-fill)
				  (text-scale-set 1)
		   (fill-column . 80)
		   (visual-fill-column-width . 80)
		   (mode . flyspell)
		   (mode . binder)
		   (mode . whitespace-cleanup)
		   (mode . visual-line)
		   (mode . visual-fill-column))))

The modes I use are:

  • flyspell – Enables real time spell-checking in the buffer.
  • binder – Enables binder navigation for moving forwards and backwards between chapters.
  • whitespace-cleanup – Strips trailing whitespace when saving a buffer.
  • visual-line – Enables soft word-wrapping.
  • visual-fill-column – Adds word-wrapping at a specific column, instead of the window's edge.

The code in the eval block turns off hard-wrapping, increases the font size, and enables olivetti-mode. olivetti centers the text in the current window which I prefer when writing words.

For taking screenshots, I have a separate Emacs configuration that contains just PHP and web-specific packages. It loads only the packages I want, and sizes the window/fonts to a width that fits on the page. Startup looks like this:

emacs --no-init-file --load "emacs-env/init.el"

The --no-init-file= option tells Emacs to ignore my usual init.el file, and then I manually load the screenshot initializer.

Publishing changes

The "standard" Leanpub plan allows the use of git for managing book files. I use git every day, so the workflow is something I'm very accustomed to. The master branch contains content that will be published, and WIP content is stored in a named branch (usually something like add-XYZ-chapter).

Once content is ready I'll push it to the remote repository and then manually publish it from within Leanpub. There is an API, but that's reserved for the "PRO" plan.

I have two Beeminder goals set up for the book:

  • write-php-book – This goal is updated any time I make a commit to the book repository. This makes sure I'm writing often enough to get things done.
  • publish-php-book – Leanpub can notify a webhook when a new version is published, so I use this to add a data point to Beeminder. This makes sure I'm publishing changes I write instead of waiting until things are "perfect".

I originally started with a word count goal. This worked well when I was trying to write all of the initial content, but once I started editing it made more sense to change to a goal that didn't focus on raw words.