Generating EPUBs for My Blog
You can read my blog posts on ur Kindle now
Making EPUBs by hand is tedious work, so I automated the process for my whole blog! At the top right corner there is now a “Read Offline” button that downloads an EPUB that you can sideload on your ereaders. If you have a Kindle, you can use your Send-to-Kindle email and it will convert it to a format your Kindle can read.
How It’s Made
Since EPUBs are just ZIP archives, I’m using JSZip to assemble them; here is a simplified example of that process. The template files are based on my work from my previous blog post. It was quite easy working with this library, so I’m excited to apply this knowledge for future projects!
import JSZip from 'jszip';
const zip = new JSZip();
// Read template files and replace $TOKENS
const toc_ncx = fs.readFileSync('src/assets/epub/toc.ncx')
.toString()
.replaceAll('$SLUG', entry.id)
.replaceAll('$TITLE', post.title);
// Add files to zip, with mimetype as the first file,
zip.file('mimetype', 'application/epub+zip');
zip.file('OEBPS/toc.ncx', toc_ncx);
// Generate zip file
const zipData = await zip.generateAsync({
type: "blob",
streamFiles: true
});
I used the same code as my RSS feed to parse the markdown into XHTML. I run an extra processing step to surround <img>
elements in a <div>
, downscale the image in sharp, convert it to a JPG, add it to the zip, and point the src
attribute to the new file location.
// Downscale image and convert
const imageBuf = await sharp(`public/${src}`)
.resize(1200, null, { withoutEnlargement: true })
.jpeg()
.toBuffer();
// Get filename and store it for later
const filename = src.match(/.+\/(.+)\..+/)[1];
images.push(filename);
// Add image to zip, and return modified <img>
zip.file(`OEBPS/images/${filename}.jpg`, imageBuf);
return `<div><img src="../images/${filename}.jpg" alt="" /></div>`;
This is run inside of the callback of an async replaceAll()
from the str-async-replace package because sharp’s toBuffer()
is async. Here’s the regex pattern it uses.
/<p><img.+?src="(.+?)".+?\/><\/p>/g
And then I replace an $IMAGES
token in the content.opf
template with the results of this function. This is necessary because EPUBs require that all files are declared in the manifest.
images.map(img => `<item id="${img}" href="images/${img}.jpg" media-type="image/jpeg"/>`).join('\r\n\t\t')
Post Mortem
This was super cool to make! I think what ereaders need is a first-class reading experience for online sources, like I am now offering for my blog. There are still some quirks with the system—emojis and certain images don’t load—but I’ll keep working on it over time as I notice any issues.
Happy readings!
