All articles

Case studies, Kajabi

How I added printable course workbooks to Kajabi (without a server)

How I built printable, branded workbooks for a Kajabi course with one script: no server, no monthly cost, and the Kajabi gotchas on the way.

By Fork Guy

An Australian professional-education institute runs its certificate programme as a cohort course on Kajabi. Their students kept asking the same thing: can we print this?

The brief was short:

A "print" button at the top of each lesson/module that gives a clean version of the page — no side menu, no website clutter — just the heading and the lesson content.

What they ended up with is a bit more than a print button: an individual lesson becomes a branded printout with its own cover, logo and copyright notice. Module, weekly and full-course workbooks also include a contents list before the lessons. It runs entirely in the student's browser, with no server, no plugin and no monthly cost.

This is how it was built, what went wrong, and what I'd do the same way again.

A sample weekly workbook: cover page, contents, and a lesson with the logo header and copyright footer

A weekly workbook: cover, contents, then each lesson on its own page. (Shown with a demo course and brand. The client's real workbooks look the same with their logo and content.)

Asking before quoting

"A print button" can mean anything from ten lines of CSS to a PDF-rendering service. So before quoting I asked seven questions:

  1. One lesson at a time, or a whole module/course at once?
  2. The browser's normal print window ("Save as PDF"), or a finished PDF that downloads instantly?
  3. What should happen to videos, quizzes and downloads?
  4. Logo, page numbers, footer, copyright?
  5. Should pages be stamped with the student's name to discourage sharing?
  6. Which courses, which students?
  7. Deadline?

The answers shaped everything:

  • Modules and the full course, not just single lessons — "workbook style".
  • Text and lesson images only. Videos, quizzes and downloads stay online, with a note on the cover pointing students back to the course.
  • Logo, copyright line and a "personal use only" notice on every page. No name stamping.
  • All courses, all enrolled students.

That second answer was the important one. Once videos and quizzes were out, nothing needed a server: the browser can do the whole job. That became the headline for the client — no hosting bill, ever.

Three ways to build it

  • Print stylesheet and the browser's print window — chosen. Cheap, no hosting, and the text stays selectable and searchable in the PDF.
  • Generate the PDF in the browser (html2pdf / jsPDF) — rejected. These screenshot the page into an image: blurry text, huge files, fragile with real content.
  • Render on a server (headless Chrome) — rejected. A monthly cost, and hard to authenticate against member-only lesson pages.

Reading the theme before writing code

The course uses Kajabi's Momentum product theme. Two things made this job far easier than it could have been:

1. All previous customisation lived in one place. Earlier work for this client (hiding publish dates in the sidebar, a breadcrumb above each lesson, auto-scrolling the sidebar, bigger quiz answer boxes) was all in the theme's Custom Code → Embedded Scripts box, written against class names rather than lesson IDs. That mattered later.

2. The theme export told me exactly where everything is. Exporting Momentum gives you its Liquid templates. From those:

  • The lesson text lives in .post-body .main-content-container. The main video, the quiz, the downloads and the comments all sit outside it — so "text only" mostly comes for free.
  • The course outline (week → module → lesson) is rendered into the sidebar of every lesson page, in order, as a flat list of siblings.
  • Lessons that haven't been released yet (drip content) don't appear in that outline at all — so a workbook can only ever contain what the student already has access to.
Gotcha #1: the theme export had the Embedded Scripts field empty. A Kajabi theme export is not a backup of your custom code. Copy it out by hand.

How the workbook works

One script, three (later four) buttons: Print lesson, Print this module, Print this week, Print full unit.

Four print buttons above a lesson title in a Kajabi course

The buttons sit above the lesson title. Course content is blurred at the client's request.

Print lesson builds a dedicated "Lesson workbook" cover, clones the current lesson content into a hidden print container, then opens the print window. The lesson title is the main cover heading; the unit/course name sits beneath it as context. The programme name appears on the cover and in the repeating lesson-page header. A single lesson needs no contents list.

Before: the Kajabi lesson page with sidebar and navigation
After: Chrome's print preview showing a clean A4 page with a logo header and copyright footer

Before: the lesson as students see it. After: just the lesson, with the logo at the top and the copyright notice at the bottom. (This shot is from before the lesson cover was added; the final version is below.)

A single-lesson printout: a cover page with the lesson title, then the lesson with logo header and copyright footer

The final "Print lesson" output: a cover led by the lesson title, then the lesson itself. (Demo brand and content.)

Print module / week / full unit:

  1. Reads the sidebar outline to get the lesson list and order.
  2. Fetches each released lesson page in the background — same origin, using the student's own session — six at a time, with a progress box ("Preparing your workbook… 120 of 324 lessons").
  3. Keeps .post-body .main-content-container, strips iframes, video, audio and forms.
  4. Skips quiz lessons and video-only lessons, but lists them in the contents as (online only). Lessons with a video and text keep the text plus a one-line "watch it online" note.
  5. Builds a cover page, a contents page, then each lesson on a fresh page.
  6. Waits for every image to load, then opens the print window.
A progress box reading "Preparing your workbook… 40 of 324 lessons"

Building the full-unit workbook: 324 lessons fetched six at a time, with a progress box and a Cancel button.

Logo and copyright on every printed page

Browsers don't give you real page headers and footers. The trick that works across browsers: make each lesson its own <table>. A table's <thead> and <tfoot> repeat on every printed page, so the logo goes in the head and the copyright in the foot.

css
1.wb-page thead { display: table-header-group; }
2.wb-page tfoot { display: table-footer-group; }
3.wb-page + .wb-page { break-before: page; }

Page breaks go between tables. Forcing a break inside one giant table cell is unreliable.

Gotcha #2: Momentum loads Bootstrap 4, and Bootstrap's print CSS sets @page { size: a3 } and body { min-width: 992px }. Printouts come out tiny. The fix is to inject your own @page { size: A4; margin: 0 } — but only while your print is running, so it doesn't affect anything else. Margin 0 also hides the browser's URL/date header line.

Small things that make it feel finished

  • Ctrl+P on a lesson gives the same cover and clean lesson content as the button.
  • The title matches the print scope. Lesson covers lead with the lesson title and show the unit as context. Module, weekly and full-unit workbooks keep their existing course/unit heading and scope label.
  • The PDF gets a sensible default file name (the script swaps document.title while printing).
  • Lazy-loaded images are forced to load before the print window opens.
  • The client's logo is an SVG built into the script — no image hosting — and every page shares one copy of it so a 600-page workbook stays light.

Testing without touching the live course

Students were using the course every day, so nothing could appear until it was ready.

  • Hidden rollout. During development, the installed script hid the print buttons unless a course page was opened with ?printtest=1, remembered in that browser; ?printtest=0 turned the preview off. That let the client review the real course while students saw nothing. After approval, the temporary flag, saved preference and test-only controls were removed from the deployment build. Printing now initialises automatically on supported course pages.
  • Console first. Before installing anything, the script was pasted into the browser console on a lesson page. It only lives in that tab; refresh and it's gone.
  • A fake Kajabi course. I generated a mock course from the theme's own templates — released and drip-locked weeks, a quiz, video lessons, a long lesson, lazy images, Bootstrap's A3 rule, and the client's existing custom code — and ran it through Playwright: the print output is turned into real PDFs and checked page by page.
The bug the tests didn't catch: "Print this module" did nothing on the live course. The button identified the current module from one read of the sidebar; the workbook builder read the sidebar again and looked the module up by object identity. A fresh read creates new objects, so the lookup always failed and the builder quietly gave up. My tests called the builder directly and never clicked the button. Fix: the module carries a reference to its own week — and the tests now click every real button. Test the path the user actually takes.

Moving everything site-wide

The client then asked for something bigger: they create a new cohort course for every intake, and Kajabi can't duplicate a cohort course. Re-pasting custom code into every new course wasn't going to last.

Kajabi has a site-wide Header Page Scripts box. Its documentation says that code runs on "every page on your website" — it doesn't say whether that includes course lesson pages. A two-line invisible test settled it: it does.

Moving the old customisation there needed a few adjustments:

  • Header scripts run before the page body exists → everything waits for the page to load.
  • Header scripts also run on the website and landing pages → CSS variables were renamed from --primary-color to --brand-*, the sidebar rule only targets the sidebar that contains a course outline, and the scripts exit early on non-course pages.
  • During the switch-over the old and new copies run side by side, so every step had to be safe to run twice (one breadcrumb, one answer box).

Finally, ~55 KB of code in a Kajabi settings box is hard to maintain, so the files are hosted on Cloudflare and Kajabi gets three lines: a comment, a stylesheet and a deferred script.

  • <link rel="stylesheet" href="https://<your-host>/course.css">
  • <script src="https://<your-host>/course.js" defer></script>
Kajabi's Header Page Scripts box with a three-line snippet at the bottom

The whole install, as Kajabi sees it: three lines in Header Page Scripts. (Other scripts and the client's details are blurred.)

The stylesheet loads before the page paints (no flash of an unformatted sidebar), the script is deferred, and a five-minute cache means fixes go live without anyone opening Kajabi.

New cohorts now get: the sidebar layout, lesson highlight, hidden publish dates, breadcrumb, larger quiz answer boxes and all four print buttons — automatically. Still set per course: choose the Momentum theme and its colours/fonts. Tip for cohort creators: Kajabi can create a new cohort by importing an evergreen course, so keep one evergreen "master copy" of the content.

Results

On the live course (18 weeks, 48 modules, 324 lessons):

  • One lesson: ready instantly; a cover plus 1–2 pages.
  • One module: a few seconds; 13 pages.
  • One week: about 15 seconds; 78 pages.
  • Full unit: about 1 minute 50 seconds; 663 pages.

The full unit started at 2 min 40 s; downloading six lessons at a time instead of four brought it under two minutes. It can't go much lower: every Kajabi lesson page carries the full 324-lesson sidebar, so the full unit means downloading ~190 MB of HTML.

(These were measured just before the final tweaks below. Those only changed cover and header text, with no extra downloads, so the times still hold.)

Going live

The client reviewed everything on their real course using the test flag and signed it off. Their feedback before launch was small and specific:

  • Put the programme name on the covers, above the unit title, and in the header of every lesson page.
  • Give a single lesson its own cover, led by the lesson title, with the unit name underneath as context. A one-lesson printout doesn't need a contents page, so it skips it.

Both went in the same day. On 9 October 2026 the buttons were switched on for every student, and the temporary test flag, its saved browser setting and the test-only helpers were deleted from the code rather than just turned off. Because the files are hosted, going live was an upload to Cloudflare, not an edit in Kajabi.

What the client has now:

  • Four print options on every lesson, in every course, including cohorts that don't exist yet.
  • No server, no subscription, nothing to renew.
  • A three-line install in Kajabi that can be removed in seconds, with fixes going live from Cloudflare within five minutes.

What I'd do the same way again

  • Ask before quoting. The client's answers removed the expensive option entirely.
  • Read the platform's real limits first. "Just duplicate the course" isn't possible for Kajabi cohorts; finding that out early changed the architecture.
  • Write custom code against class names, keep it in one place. It's why "move everything site-wide" was a move, not a rebuild.
  • Ship hidden, then remove the temporary controls. A test flag let the client review the real course during rollout. The finished deployment build no longer needs a test URL or saved browser preference.
  • Verify what's actually running. At one point a deployment silently shipped the old file and an "it still doesn't work" was really the previous version. Exposing a version number (printTool.version in the console) ended that confusion instantly.

Running a course on Kajabi and need something the platform doesn't do out of the box? Tell me about your project.

Have a similar problem?

Send a short brief describing your platform and what you want to fix or build. See the services for the kinds of work I take on.