Novellas with groff
blog - git - desktop - contact
2026-10-31
I recently finished my second "novella".
Don't even think about it: I won't publish this. Nor send it to you privately, which is mostly the same as publishing it.
The first story is roughly 50 A5 pages long (~15k words), the second one is about 120 pages (~40k words). (Both barely fit the definition of a "novella", apparently, with the first one being a bit too short and the second one a tiny bit too long.)
It feels pretty absurd and surreal that I have done this at all. In school, anything related to "writing" was pure horror for me -- unless it was related to maths, physics, or other "technical" stuff. But here we are.
How long did it take? Well, the first one took over seven months. But the second one, the longer one? Ten days.
Some time last year, probably around July, I started reading some of the books that were lying around in my appartment for decades. And this was much more enjoyable than I thought. No idea why. It's not like I never read a book before, but it was always a somewhat boring thing to do and it took me ages -- unless it was something technical. I'm only referring to fiction and novels here. Like, one book per year, tops. Yet, I somehow got hooked last year and now I've read about 30 novels since July 2025. Some faster (a couple of days), some slower (three months).
It wasn't that much of a stretch to try to do this myself.
This blog is still technical in nature, so I'll walk through the software that I used.
Table of contents:
Both of my books (they're not really "books", but I'll keep referring to them as such for the sake of simplicity) are typeset in groff. You know, the thing that usually renders manual pages in your terminal on Linux. groff/troff is a full typesetting system, though, and it can be used to produce real books, which I've already mentioned a couple of times in previous blog posts.
In my case, the appeal of groff is that it's relatively small and fast, but still able to produce high-quality output. Making that 50-page document takes about 700 ms on my 13.5 year old PC, and the 120-page one takes 1.6 seconds. This includes some pre-processing which we'll cover later. Given that most modern software these days feels like walking in mud, this is refreshingly quick.
Because it's more fun that way, I made my own little macro package. There really isn't a lot to it, though. It's tiny. Not a lot is needed for a simple novel / novella -- no table of contents, no cross-references, no diagrams, no code blocks, no lists, ... Just nicely flowing text and sometimes a word gets emphasized.
Disclaimer: You'll note some TODO items in the text below. groff is a full-blown typesetting system and these things are complicated. I'm a long way from having mastered every aspect of it.
There are only three macros:
.\" Section header / chapter title .de SH . sp 1v . ps +5p . ce \f[HR]\\$1\fP . ps . sp 4v .. .\" Normal paragraph with hanging indent. .de PP . br . ti +2m .. .\" "Long" paragraph, longer empty space. .de LP . sp 2v ..
(The name
SHis a spill-over from manual pages. I've written quite a few of those and this name somehow stuck.
CHfor "chapter heading" or
CTfor "chapter title" might have been better choices. Or, you know,
.CHAPTER.)
You start defining a macro with
.de-- anything until
..is part of that macro.
.SH: Go down one
HRfont (Helvetica), return to previous type size, add another four vees of space.
.PP: Just break the current line, then request a temporary indent for the next output line. This creates a hanging indent, which is customary in German books.
.LP: Just some empty space between paragraphs. Used for things like intra-chapter time jumps.
You can look up the individual commands in
man 7 groff.
And that would already be enough to produce a document that somewhat resembles a book.
Put the snippet above in a file called
macros.groff. I made one file per chapter. For example:
chapter01.groff. You can then make a PDF using a command like this:
groff -Tpdf -dpaper=a5 -P-pa5 macros.groff chapter01.groff >book.pdf
A5 is roughly the page size of a "typical book".
You'll note that there are no page numbers by default. You could actually just decide to not have them if all you want is a digital PDF. They're useful for printing, but you don't really need them in a file, if we're being honest.
Except: The thing gets confused about page breaks. Create the PDF using the example above and you'll see:
Looks like the baseline of the last line of output is just outside the page. Not sure what's going on here -- it's related to setting the page size to A5. In other words, I probably missed another spot where this has to be set. It'll be fixed implicitly by the following.
The whole page number thing consists of three parts: First, how large is the footer (the area where the page numbers live)? I defined this in a register (which is not really necessary but I find the "code" easier to read this way) -- the name is arbitary:
.nr _FooterSize 5v
We than plant a trap at the bottom of the page:
.wh -\n[_FooterSize]u _FooterTrap
\n[_FooterSize]"gets" the value of the register called
_FooterSize. There's a minus sign in front of it, it's negative and relative to the bottom of the page. The documentation says:
A general-purpose register (one created or updated with the nr request) is implicitly dimensionless, or reckoned in basic units if interpreted in a measurement context.
I'm not 100% sure why I need to add the
u. I haven't read every page of the manual, yet. Needless to say, it doesn't work without it.
Okay, so the trap gets installed at 5v before the end of the page and it'll call the
_FooterTrapmacro. It looks like this:
.de _FooterTrap
.\" Push current environment on stack and switch to `_FooterEnv`.
.\" Parent env will keep partially collected lines.
. ev _FooterEnv
.\" Copy settings from default environment.
. evc 0
.\" Don't do this for pages 1 and 2, they're title pages in my actual
.\" "book".
. if \\n[%]>2 \{\
.\" Set vertical position, explained in the text below.
. sp |\\n[.p]u-2v
.\" Print % register centered.
. ce
\fR\\n[%]\fP
\}
.\" Go back to previous environment.
. ev
.\" Request page break.
' bp
..
It's much easier to read without the inline comments:
.de _FooterTrap
. ev _FooterEnv
. evc 0
. if \\n[%]>2 \{\
. sp |\\n[.p]u-2v
. ce
\fR\\n[%]\fP
\}
. ev
' bp
..
Why is it
'bpinstead of
.bp? We'll get to that.
What I'm unsure about: I set the vertical position to
|\\n[.p]u-2v. That's relative to the top of the page, it's the page length minus two vee. I would have expected
|-2v-1v(including the "minus one vee" mentioned in the docs) to also work, but that leads to an infinite loop (probably triggers the same trap again and again). Why?
For symmetry, I also want some empty space at the top of the page:
.nr _HeaderSize 5v .de _HeaderTrap ' sp |\\n[_HeaderSize]u .. .wh 0v _HeaderTrap
And another TODO for me: This isn't 100% symmetrical. The length of the header is a bit larger than the footer. An issue with the page size again?
Let's have a closer look at traps. I'm not 100% sure on this -- this is to the best of my understanding.
Actually, let's take a step back from the example above. Let's start with this:
This is a little excerpt from Mary Shelly's Frankenstein. These visions faded when I perused, for the first time, those poets whose effusions entranced my soul and lifted it to heaven. I also became a poet and for one year lived in a paradise of my own creation; I imagined that I also might obtain a niche in the temple where the names of Homer and Shakespeare are consecrated. You are well acquainted with my failure and how heavily I bore the disappointment. But just at that time I inherited the fortune of my cousin, and my thoughts were turned into the channel of their earlier bent. Six years have passed since I resolved on my present undertaking. I can, even now, remember the hour from which I dedicated myself to this great enterprise. I commenced by inuring my body to hardship. I accompanied the whale-fishers on several expeditions to the North Sea; I voluntarily endured cold, famine, thirst, and want of sleep; I often worked harder than the common sailors during the day and devoted my nights to the study of mathematics, the theory of medicine, and those branches of physical science from which a naval adventurer might derive the greatest practical advantage. Twice I actually hired myself as an under-mate in a Greenland whaler, and acquitted myself to admiration. I must own I felt a little proud when my captain offered me the second dignity in the vessel and entreated me to remain with the greatest earnestness, so valuable did he consider my services.
Save it as
test.groffand make a PDF:
groff -Tpdf test.groff >book.pdf
Looks like this:
Install a trap at 10v. It's empty so far and does nothing -- insert this at the very top of the file:
.de MyTrap .. .wh 10v MyTrap .\"
The PDF looks the same.
Now what happens when we change the trap macro to this?
.de MyTrap FOO ..
This is the result:
When the output reached the word "fishers", it sprang the trap. "FOO" comes after "fishers" in the PDF: "fishers" triggered the trap, then "FOO" got inserted. I think it's as simple as that, isn't it?
Suppose we want to put "FOO" at the beginning of the line and we want nothing but that word on that line. "fishers" shall be put on the next line.
If I understand correctly, switching environments is key: The environment holds the currently collected line (among many other things). Switch to a new one and we get a new buffer/line -- one that's empty so far. We can then flush/break this line to have it included in the PDF, and then switch back to the previous environment:
.de MyTrap . ev MyTrapEnv FOO . br . ev ..
So far, so good. Now I want there to be an empty line before and after "FOO":
.de MyTrap . ev MyTrapEnv . sp FOO . sp . ev ..
In the new environment, we issue
.sp: It breaks and inserts space. Nothing was collected so far, so the break doesn't print anything at this point. Then we have
FOO-- this isn't printed directly, it's just collected. The next
.spbreaks again (thus printing "FOO") and then inserts more space.
Okay. Now. Suppose that printing "FOO" with those two lines around it is all we want to do inside of the trap. We then want the normal text to continue at some absolute position on the page, say at 17v.
So, how about the following? We go back to the previous environment and then issue an absolute location request:
.de MyTrap . ev MyTrapEnv . sp FOO . sp . ev . sp |17v ..
Oops:
.sp |17vbroke the already collected line (the one with just "fishers" in it) and then inserted the space. We don't want that break. (Also note that "fishers" is at the exact same position where we left off inside the trap.)
This can be avoided by using
'instead of
.:
' The neutral apostrophe is the no-break control character, rec‐
ognized where the control character is. It suppresses the
(first) break implied by the bp, ce, cf, fi, fl, "in", nf, rj,
sp, ti, and trf requests. The requested operation takes ef‐
fect at the next break.
So let's do this:
.de MyTrap . ev MyTrapEnv . sp FOO . sp . ev ' sp |17v ..
Result:
The example in the documentation doesn't change environments. But I find it much easier to understand what's going when changing them. (And I hope I got it right.)
One final remark: Environment carry settings.
.ev foostarts a completely new environment (unless an environment with that name already exists), it doesn't clone the previous one. That's why my macro package also does
.evc 0.
As a little exercise, see what happens when you use
'sp |12v,
'sp |11.5v,
'sp |11v, all the way down to
'sp |9v.
Only when writing this blog post, I found the following sentence in the docs (Basics):
If you don't start a sentence on a new line, put two spaces after the previous sentence. GNU troff then recognizes punctuation that ends a sentence, and inserts inter-sentence space accordingly.
I didn't do that, because this isn't customary in German. But now I see it. Sometimes, it looks like this in the PDF:
Foo. Bar.
And sometimes there's a little bit of extra space:
As far as I know, this is usually not done in German books. I disabled it with this request, which keeps "minimal inter-word spacing" at the default value of 12 but sets "supplemental inter-sentence spacing" to 0:
.ss 12 0
This isn't part of my macro package, but it comes first in the "index" file (
book.groff, see demo package below).
Apparently, it's as simple as this:
.hla den .hpf hyphen.den
denis "de(utsch) new": This includes the "new" German rules from 2006.
I actually wrote both of my stories in plain groff, using the macros I explained above.
Markdown has the big advantage of being "visually pleasing": It mostly looks like text that is intended to be read by a human being. groff isn't, really. I mean, it wasn't horrible, I wouldn't have written over 170 pages in groff if this was pain. It's fine. I use groff in several other places as well. But Markdown is a tiny little bit better, and I assume that it allows me to focus more on the creative process.
So, afterwards, I convert