bujo.js is a customizable library for generating Bullet Journal-style PDFs using JavaScript. It enables users to create organized PDF templates with customizable options, such as cover pages, milestones, undated calendars, planning pages, and more.
The layout engine is fully metric (millimetres) and scale-aware: every page — cover, index, future log, milestones, year at a glance and the monthly planning sections — is laid out relative to the selected paper size, so the same journal renders correctly from A6 pocket books up to A3 desk planners.
- Cover Page: A clean typographic cover with a delicate double frame and year fill-in.
- Index Page: A two-column index pre-filled with the real page number of every generated section.
- Future Log: An intro page plus twelve undated month pages with true 7×6 calendar grids and notes.
- Milestones: Top 30 milestones (two columns of checkbox rows) and a top 10 yearly milestones page.
- Year at a Glance: Twelve mini month panels for a helicopter view of the year.
- Dotted Grid Pages: Classic 5 mm bullet-journal dot grid across the full content area.
- Daily Plan: Morning/afternoon/evening time blocks, top priorities, to-dos and a dotted notes band.
- Weekly Overview: Seven day panels plus an accented Goals & Habits panel.
- Habit Tracker: A 31-day × 8-habit matrix with goal-setting and flexible tracking space.
- Handwriting Practice: Four-line copybook ruling with a shaded middle band, as used in Indian schools.
- Pages for Children and Students: Reading log, weekly study planner, 30-day practice tracker and a name cover.
- Printable Editions: Five ready-made books and a build script that writes them, with a manifest, as byte-identical PDFs.
- Color Schemes: A refined
colorpalette (slate ink, muted petrol accent) and amonochromepalette. - Browser and Node:
createBulletJournalBook()saves a download in the browser;generate()returns the jsPDF document so services can stream it (doc.output('arraybuffer')).
npm install github:vahinitech/bujo.jsor clone the repository:
git clone https://github.com/vahinitech/bujo.js.git
cd bujo.js && npm installThe package is ESM ("type": "module"); import it with:
import { BulletJournal, PAPER_SIZES } from 'bujo.js';const journal = new BulletJournal('My Custom Journal', 'monochrome');In the browser (triggers a download):
journal.createBulletJournalBook('A4');In Node (e.g. an HTTP service):
const doc = journal.generate('A4');
const pdfBuffer = Buffer.from(doc.output('arraybuffer'));Supported paper sizes (portrait, millimetres): A3 (297×420), A4 (210×297), A5 (148×210), A6 (105×148).
Every page method draws onto a jsPDF document. Create one with the right physical page size via createDocument, then add the sections you want:
const doc = journal.createDocument('A5');
const paper = PAPER_SIZES.A5;
journal.addCoverPage(doc, paper);
journal.addIndexPage(doc, paper);
journal.addTopMilestonesPage(doc, paper);
journal.addDottedGridPage(doc, 5, paper); // 5 mm dot spacing
journal.addDailyPlanningPage(doc, paper);
journal.addWeeklyOverviewPage(doc, paper);
journal.addFlexibleTrackingPage(doc, paper);
doc.save('custom_journal.pdf');Legacy inch-based dimensions (e.g. { width: 8.27, height: 11.69 }) and inch dot spacings (0.2) are detected and converted automatically for backwards compatibility.
| Method | Description |
|---|---|
generate(paperSize, doc?) |
Builds the full journal, returns the jsPDF document (no save) |
createBulletJournalBook(paperSize, doc?) |
Builds the full journal and saves it |
createDocument(paperSize) |
Creates a jsPDF document with the correct physical page size |
getExpectedPageCount() |
Total pages of a full book (66) |
addCoverPage(doc, paper?) |
Cover on the current page |
addIndexPage(doc, paper?) |
Index with real section page numbers |
addUndatedCalendarPages(doc, paper?) |
Future log intro + 12 undated month calendars |
addTopMilestonesPage(doc, paper?) |
Top 30 + top 10 yearly milestones (2 pages) |
addHelicopterOverviewPage(doc, paper?) |
Year at a glance |
addDottedGridPage(doc, spacingMm, paper) |
Full-page dot grid |
addDailyPlanningPage(doc, paper) |
Daily plan |
addWeeklyOverviewPage(doc, paper) |
Weekly overview |
addFlexibleTrackingPage(doc, paper, opts?) |
Habit tracker + goals; opts.kicker, opts.monthLine |
addNameCoverPage(doc, paper, opts?) |
Cover with a "This journal belongs to" line, on the current page |
addHowToUsePage(doc, paper, opts?) |
Numbered instructions, optional key to the four lines |
addHandwritingPracticePage(doc, paper, opts?) |
Four-line handwriting ruling |
addReadingLogPage(doc, paper) |
Reading log with stars to colour in |
addStudyPlannerPage(doc, paper, opts?) |
One school week with goal and exams boxes |
addPracticeTrackerPage(doc, paper, opts?) |
30-day practice tracker |
These draw on the same document as the other sections and follow the same margins and colour scheme on every paper size.
const doc = journal.createDocument('A4');
const paper = PAPER_SIZES.A4;
journal.addNameCoverPage(doc, paper, {
title: 'My Handwriting Journal',
subtitle: 'Ten minutes a day of careful writing',
kicker: 'Practice Every Day'
});
journal.addHandwritingPracticePage(doc, paper, { title: 'Letters a to m' });
journal.addReadingLogPage(doc, paper);
journal.addStudyPlannerPage(doc, paper, { week: 1 });
journal.addPracticeTrackerPage(doc, paper);addHandwritingPracticePagedraws sets of four evenly spaced lines, as in Indian school copybooks. Small letters fill the shaded band between lines 2 and 3 and sit on line 3, which is the darkest; line 2 is dashed; tall letters and capitals reach line 1 and tails reach line 4. A margin line runs down the left, with name and date lines at the top. Options:bandMm, the height of one set (default 12 mm on A4, between 8 and 16 mm on other papers, because children write at much the same size on any page);rows, to draw fewer sets than fit;titleandkickerfor the heading. The number of sets comes from the page height.addReadingLogPageis a table with Date, Book title, Pages or minutes and How did it go?, where each row has three star outlines to colour in. The stars are drawn from lines, since the standard PDF fonts have no emoji.addStudyPlannerPagehas "This week's goal" and "Exams and tests coming up" boxes above a Monday to Sunday table with Subject, Homework or topic and a Done checkbox on each line.opts.weekputs "Week n" above the title.addPracticeTrackerPagehas 30 numbered day boxes in 5 rows of 6, each with a checkbox, and boxes for "Letters I find hard" and "Letters I improved".addNameCoverPageuses the existing cover frame withtitle(default: the journal title),subtitleandkicker, and a "This journal belongs to" line. LikeaddCoverPage, it draws on the current page.addHowToUsePageprintsopts.itemsas a numbered list underopts.title.opts.rulingGuide: trueadds an enlarged four-line set with a note beside each part.
The constructor takes an optional third argument:
const journal = new BulletJournal('My Handwriting Journal', 'color', {
footer: 'Free handwriting check: vahinitech.com'
});When footer is set, every page gets that text centred in small grey type in the bottom margin, on the same line as the page label and number; on covers it sits inside the frame. It never overlaps page content. If a long page label would run into it, the label is left out. Without the option nothing changes, and new BulletJournal(title, colorScheme) works as before.
owner prints a name on the cover: on the "This journal belongs to" line of a name cover, and under the tagline of the bullet-journal cover. It is trimmed, inner spaces are collapsed, and it is cut at 40 characters (MAX_OWNER_CHARS); a long name is set smaller so it stays on the line.
const journal = new BulletJournal('My Handwriting Journal', 'color', { owner: 'Vahini' });The built-in PDF fonts only draw Latin-1 text, so a name in another script (Telugu, Hindi, Tamil...) is refused with an error rather than printed as stray symbols. fitsStandardFonts(name) tells a caller in advance. Without owner, the cover keeps its blank line to write on and nothing else changes.
EDITIONS lists ready-made books and buildEdition(id, paperSize, options) builds one and returns the jsPDF document. options takes footer, colorScheme (default color) and owner.
import { EDITIONS, buildEdition } from 'bujo.js';
const doc = buildEdition('kids-handwriting-journal', 'A5', {
footer: 'Free handwriting check: vahinitech.com'
});
const pdf = Buffer.from(doc.output('arraybuffer'));| id | Title | Pages | Contents |
|---|---|---|---|
kids-handwriting-journal |
My Handwriting Journal | 29 | Name cover, how to use, practice tracker, 24 practice pages, 2 reading logs |
handwriting-practice-pages |
Handwriting Practice Pages | 11 | Name cover, 10 practice pages |
student-study-planner |
Student Study Planner | 17 | Name cover, 12 weekly planners, 2 habit trackers, 2 reading logs |
habit-tracker |
Habit Tracker | 13 | Cover, 12 undated monthly habit trackers |
bullet-journal |
Bullet Journal | 66 | The full book from generate() |
Each entry has id, title, description (one sentence), pages (the exact count on every paper) and papers (['A4', 'A5']). The page counts are checked by the tests.
Editions are reproducible: they get a fixed creation date, a file id derived from the edition, paper, footer, colour scheme and owner name, and fixed document properties, so the same input always gives the same bytes.
npm run build:printables -- --footer "Free handwriting check: vahinitech.com"
# or
node scripts/build-printables.mjs [--footer "text"] [--out dir]It writes every edition on A4 and A5 to dist/printables/<id>-<paper>.pdf (for example kids-handwriting-journal-a5.pdf) and one manifest.json:
{
"generator": "bujo.js",
"version": "1.0.0",
"commit": "<git HEAD sha, or null outside a git checkout>",
"footer": "Free handwriting check: vahinitech.com",
"files": [
{
"id": "kids-handwriting-journal",
"title": "My Handwriting Journal",
"description": "A month of daily handwriting practice for children, with a practice tracker and a reading log.",
"paper": "A4",
"pages": 29,
"file": "kids-handwriting-journal-a4.pdf",
"bytes": 64679,
"sha256": "<hex>"
}
]
}footer is null when --footer is not given. --out writes somewhere other than dist/printables. The script stops if a built file does not have the page count its edition declares. It needs no network.
Serve the repository with any static file server and open the generator UI:
npx serve .
# then open http://localhost:3000/public/npm test
npm run lintThe page and edition tests run against the real jsPDF: they check that every drawing stays inside the content area from A6 to A3, that the footer is on every page and clear of everything else, the page count of every edition, and that two runs of the build script give byte-identical files.
- jsPDF ^4 — the only runtime dependency.
This project follows Semantic Versioning (SemVer) via semantic-release. Version numbers are automatically updated based on conventional commits:
fix: <description>– patch release.feat: <description>– minor release.feat!: <description>/BREAKING CHANGE:– major release.
- Fork the repo.
- Create a feature branch (
git checkout -b feature/YourFeature). - Commit changes (
git commit -m 'feat: add new feature'). - Push to the branch (
git push origin feature/YourFeature). - Open a Pull Request.