Jun 19, 2026 4 min read

How to Add a PDF Download Button to Ghost CMS Posts

Learn how to add a PDF download button to Ghost CMS posts using jsPDF. Generate downloadable PDFs directly from Ghost articles with this step-by-step guide.

How to Add a PDF Download Button to Ghost CMS Posts

Ghost is one of the best publishing platforms available today, but there are times when publishers need functionality beyond what comes out of the box.

One feature we are frequently asked about is the ability to allow readers to download articles as PDF files. Whether you're publishing research reports, educational content, premium membership articles, whitepapers, or long-form magazine content, downloadable PDFs can provide a much better reading experience for users who want offline access.

Recently, we implemented a custom PDF generation feature for a Ghost publication that allowed readers to download any article directly from the post page. The solution is lightweight, requires no server-side processing, and can be integrated into most Ghost themes with minimal modifications.

In this guide, we'll walk through the complete implementation.

Why Add PDF Downloads to Ghost?

While Ghost allows content to be shared, bookmarked, and distributed through newsletters, there is currently no built-in feature for exporting individual posts as PDF files.

Adding PDF downloads can be useful for:

  • Research publications
  • Educational websites
  • Digital magazines
  • Premium membership content
  • Whitepapers and reports
  • Documentation sites
  • Long-form editorial content

For membership-driven publications, downloadable PDFs can also become an additional premium feature for paid subscribers.

How This Solution Works

The implementation uses JavaScript and the jsPDF library to generate a PDF directly in the visitor's browser.

The workflow is simple:

  1. The visitor clicks a "Download as PDF" button.
  2. JavaScript extracts the article content.
  3. The content is formatted for PDF output.
  4. jsPDF generates the document.
  5. The PDF is automatically downloaded.

Because everything happens in the browser, there is no need to generate or store PDF files on your server.

Step 1: Add a Download Button

Open your Ghost theme's post.hbs file and place the following code where you would like the download button to appear.

<div class="pdf-download-container">
    <button id="downloadPdfBtn" class="pdf-download-btn" type="button">
        Download as PDF
    </button>

    <div id="pdfLoading" class="pdf-loading" hidden>
        Generating PDF, please wait...
    </div>

    <div id="pdfSuccess" class="pdf-success" hidden>
        PDF downloaded successfully.
    </div>
</div>

Most publishers place this button either below the article header or at the bottom of the article.

Step 2: Add a Dedicated Content Wrapper

To ensure the script can reliably identify your article content, wrap the Ghost content in a dedicated container.

<section id="postContent" class="gh-content gh-canvas">
    {{content}}
</section>

The ID is important because the JavaScript will use it to extract content from the page.

Step 3: Add Styling

Add the following CSS to your theme.

.pdf-download-container {
    margin: 32px 0;
    text-align: center;
}

.pdf-download-btn {
    cursor: pointer;
    padding: 12px 18px;
    border: 0;
    border-radius: 6px;
    font-weight: 600;
}

.pdf-loading,
.pdf-success {
    margin-top: 10px;
    font-size: 14px;
}

.pdf-success {
    color: #238636;
}

You can style the button however you like to match your Ghost theme.

Step 4: Load jsPDF

Add the jsPDF library before your closing </body> tag.

<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>

jsPDF is responsible for creating the PDF document in the browser.

Step 5: Add the PDF Generation Script

The following script extracts content from your Ghost post and converts it into a downloadable PDF.

<script>
document.addEventListener('DOMContentLoaded', function () {
    const button = document.getElementById('downloadPdfBtn');
    const loading = document.getElementById('pdfLoading');
    const success = document.getElementById('pdfSuccess');

    if (!button) return;

    button.addEventListener('click', generatePDF);

    async function generatePDF() {
        const { jsPDF } = window.jspdf;

        loading.hidden = false;
        success.hidden = true;
        button.disabled = true;

        try {
            const postTitle = document.querySelector('h1')?.textContent?.trim() || document.title;
            const postContent = document.getElementById('postContent');
            const siteName = document.querySelector('meta[property="og:site_name"]')?.content || window.location.hostname;

            if (!postContent) {
                throw new Error('Post content wrapper not found.');
            }

            const pdf = new jsPDF();
            const margin = 20;
            const pageWidth = pdf.internal.pageSize.getWidth();
            const pageHeight = pdf.internal.pageSize.getHeight();
            const maxWidth = pageWidth - margin * 2;
            let y = margin;

            pdf.setProperties({
                title: postTitle,
                subject: 'Article',
                creator: siteName
            });

            pdf.setFont('helvetica', 'bold');
            pdf.setFontSize(18);

            const titleLines = pdf.splitTextToSize(postTitle, maxWidth);
            pdf.text(titleLines, margin, y);
            y += titleLines.length * 8 + 10;

            pdf.setFont('helvetica', 'normal');
            pdf.setFontSize(10);
            pdf.text(`Source: ${window.location.href}`, margin, y);
            y += 12;

            pdf.setDrawColor(200, 200, 200);
            pdf.line(margin, y, pageWidth - margin, y);
            y += 12;

            const blocks = postContent.querySelectorAll('h2, h3, h4, p, blockquote, li');

            blocks.forEach(function (block) {
                const text = block.textContent.replace(/\s+/g, ' ').trim();
                if (!text) return;

                const tag = block.tagName.toLowerCase();

                if (tag === 'h2') {
                    pdf.setFont('helvetica', 'bold');
                    pdf.setFontSize(15);
                    y += 6;
                } else if (tag === 'h3') {
                    pdf.setFont('helvetica', 'bold');
                    pdf.setFontSize(13);
                    y += 5;
                } else if (tag === 'blockquote') {
                    pdf.setFont('helvetica', 'italic');
                    pdf.setFontSize(11);
                } else {
                    pdf.setFont('helvetica', 'normal');
                    pdf.setFontSize(11);
                }

                const prefix = tag === 'li' ? '• ' : '';
                const lines = pdf.splitTextToSize(prefix + text, maxWidth);

                lines.forEach(function (line) {
                    if (y > pageHeight - margin) {
                        pdf.addPage();
                        y = margin;
                    }

                    pdf.text(line, margin, y);
                    y += 6;
                });

                y += 4;
            });

            const safeFilename = postTitle
                .toLowerCase()
                .replace(/[^a-z0-9]+/g, '-')
                .replace(/(^-|-$)/g, '');

            pdf.save(`${safeFilename || 'article'}.pdf`);

            success.hidden = false;
        } catch (error) {
            alert('Could not generate the PDF. Please try again.');
            console.error(error);
        } finally {
            loading.hidden = true;
            button.disabled = false;

            setTimeout(() => {
                success.hidden = true;
            }, 3000);
        }
    }
});
</script>

What This Script Supports

This implementation automatically handles:

  • Article titles
  • Paragraphs
  • H2 headings
  • H3 headings
  • H4 headings
  • Lists
  • Blockquotes
  • Multi-page PDFs
  • Automatic file naming
  • Source URL attribution

For many publishers, this is more than enough to create a useful downloadable version of an article.

Possible Improvements

Depending on your requirements, you can extend the implementation to support:

Feature Images

Feature images can be embedded directly into the PDF before the article content begins.

Author Information

You can pull Ghost author data and include it in the PDF header.

Publication Dates

Many publishers choose to include publication dates underneath the article title.

Membership Restrictions

Ghost's membership visibility features can be used to display the PDF download button only to paid subscribers.

Custom Branding

You can add your publication logo, colors, and custom footer information to create branded PDF exports.

When Should You Use This?

This approach works particularly well for:

  • Research websites
  • Think tanks
  • Educational publishers
  • Membership communities
  • Industry reports
  • Premium newsletters
  • Digital magazines

Because the PDFs are generated on demand, there is no additional storage overhead and no need for a separate PDF generation service.

Final Thoughts

Ghost gives publishers a powerful foundation, but many growing publications eventually need custom functionality tailored to their audience.

Adding PDF downloads is a relatively small enhancement that can significantly improve accessibility, reader convenience, and the value of premium content.

With a few template modifications and a lightweight JavaScript library, you can give readers the ability to save and archive your articles directly from your Ghost website.

At Inoryum, we specialize in custom Ghost CMS development, membership systems, Stripe integrations, middleware solutions, and advanced publishing workflows for modern digital publications.

Related Insights

View All Articles