Search by

manuxi / sulu-pdf-bundle

manuxi

PDF download for Sulu content: renders the public page of an article, event or page and sets its content into a designed PDF layout

Package info

github.com/manuxi/SuluPdfBundle

Type:symfony-bundle

pkg:composer/manuxi/sulu-pdf-bundle

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.3.1 2026-10-01 15:19 UTC

This package is auto-updated.

Last update: 2026-10-01 15:21:15 UTC


README

php workflow symfony workflow License: MIT GitHub Tag Supports Sulu 3.0 or later

🇩🇪 Deutsche Version

PDF download for Sulu 3 content. The bundle renders the public page of an article, event or page, reduces it to its content and sets that into a designed PDF layout (logo, brand color, font, page numbers, QR code to the online version, optional company data). Nothing has to be built twice: every block template you already have keeps working, because the PDF is made from the rendered HTML.

How it works

  1. GET /pdf/{resourceKey}/{id}?locale=de looks up the profile for the resource key (articles, events, pages, ...). No profile or PDF switched off: 404.
  2. The public page is rendered through a sub-request.
  3. The rules of the profile (CSS selectors) pick title, header data, hero image, lead and main content. Scripts, forms, buttons, icons and carousel chrome are dropped, lazy images get their real URL, figures and galleries are rebuilt as tables (dompdf cannot lay out block images inside figure reliably).
  4. The content is set into @SuluPdf/document.html.twig and converted with dompdf. Logo, rule and footer with the page count are drawn on every page.

Requirements

  • PHP 8.2+, Sulu 3.x
  • ext-intl, ext-dom

Installation

composer require manuxi/sulu-pdf-bundle

Register the bundle in config/bundles.php (Symfony Flex does this for you):

Manuxi\SuluPdfBundle\SuluPdfBundle::class => ['all' => true],

Import the route, config/routes/sulu_pdf.yaml:

sulu_pdf:
    resource: '@SuluPdfBundle/Resources/config/routes.yaml'

Link to the PDF in a template:

<a href="{{ sulu_pdf_url('events', event.id, app.request.locale) }}">Download as PDF</a>

Configuration

config/packages/sulu_pdf.yaml, all keys are optional:

sulu_pdf:
    paper: A4
    logo: 'public/images/logo.{locale}.png'   # PNG or JPG; "{locale}" is replaced; without a logo the site name is printed
    logo_width: 50                            # mm
    site_name: 'Example Inc.'
    colors:
        primary: '#2f6fed'                    # rule, accents, boxes; darker/lighter shades are derived
        text: '#2d2d2d'
        muted: '#777777'
    font:                                     # without files the built-in "DejaVu Sans" is used
        family: 'Open Sans'
        regular: 'assets/fonts/open-sans-regular.ttf'
        italic: 'assets/fonts/open-sans-italic.ttf'
        bold: 'assets/fonts/open-sans-700.ttf'
        bold_italic: 'assets/fonts/open-sans-700italic.ttf'
    excerpt:                                    # PDF switches in the excerpt tab
        pages: true
        articles: true
        events: true
    company_data_provider: App\Pdf\CompanyDataProvider
    profiles:
        offers:
            resource_key: offers                # a profile of its own
            rules:
                main: 'main > .container'
            options:
                company_data: end               # none | footer | end
        events:                                 # a bundled profile (event bundle installed): rules and options are tuned here
            rules:
                hero: '.event .card > img'
                remove: ['.event .location']
            options:
                company_data: end
        articles:
            rules:
                main: '.post-body'

Paths are absolute or relative to the project directory.

Profiles

A profile says for one kind of content whether a PDF exists, with which options, and where the parts of its page are.

Articles (automatic)

Recommended: the switches in the excerpt tab (excerpt.articles: true, see below). Their default rules match the article templates of the reference theme (.article-main, .article-header, ...), other themes override them as shown above.

Legacy: with manuxi/sulu-article-configuration-bundle 1.4.x (and without excerpt.articles) the profile articles reads that bundle's own switches (tab "Configuration"). Version 2.0 of the article configuration bundle no longer has these fields, so with 2.x use excerpt.articles.

Switch in the excerpt tab (pages, articles, events)

sulu_pdf:
    excerpt:
        pages: true
        articles: true
        events: true

adds a PDF section to the excerpt tab of that content, as its last section (download on/off, image captions, last-modified date, link/QR code, company data; articles also have the author box), and registers the profile (pages, articles, events) that reads these values. The switches use Sulu's own hook for extra excerpt fields (sulu_content.content_excerpt_form), so they are saved in the content's excerpt data: per language, and with the draft/publish workflow like any other content. Only the published version decides whether the download exists.

  • Pages have no author box. Without excerpt.pages define a profile with resource_key: pages in the config - then every page has a PDF.
  • Articles: with excerpt.articles the excerpt switches replace the PDF switches of the article configuration bundle (its profile is then not registered). Without it that bundle's tab "Configuration" decides (see above).
  • Events: with excerpt.events the excerpt switches decide per event; without it on/off and the options come from sulu_pdf.profiles.events for all events. Date and venue are printed as header lines from the event's own data (not scraped from the page), the event's last change is used for "last modified".

To show a link only where a PDF exists:

{% if sulu_pdf_available('pages', uuid, app.request.locale) %}
    <a href="{{ sulu_pdf_url('pages', uuid, app.request.locale) }}">Download as PDF</a>
{% endif %}

Profiles from config

Every profile with a resource_key in sulu_pdf.profiles offers a PDF for all resources of that key, with the options given there (show_captions, show_author, show_modified, show_online_link, company_data).

Profiles in PHP

Implement Manuxi\SuluPdfBundle\Profile\PdfProfileInterface and tag the service sulu_pdf.profile:

final class ProductProfile implements PdfProfileInterface
{
    public function getName(): string { return 'products'; }
    public function getResourceKey(): string { return 'products'; }
    public function getDefaultRules(): PdfRules { return new PdfRules(main: '.product-detail'); }
    public function getOptions(string $id, string $locale): ?PdfOptions { return new PdfOptions(showAuthor: false); } // null = no PDF
    public function getModified(string $id, string $locale): ?\DateTimeInterface { return null; }
}

A profile can additionally implement Manuxi\SuluPdfBundle\Profile\PdfMetaProviderInterface (getMeta(string $id, string $locale): array) to print header lines from the content's own data, such as a date or a venue, in front of the entries the meta rule finds.

Rules

CSS selectors; single-part rules are searched inside root, the first match wins.

Rule Default Meaning
root body container that holds the header data
title h1 title (also removed from the content, the layout prints it)
overline, subtitle, lead - optional header texts (removed from the content when found inside it)
badges, meta - every match becomes one badge / one meta entry (removed from the content when found inside it)
hero - container of the hero image (printed once at the top)
main main the content; falls back to <main>, then <body>
gallery - container of a gallery: its pictures are printed in two columns
author - author box, removed when "author" is switched off
remove - more elements to drop; adds to the built-in list (nav, footer, scripts, ...)
exclude_figures - figures that keep their markup

Company data

The bundle has no company data of its own. Implement Manuxi\SuluPdfBundle\Service\CompanyDataProviderInterface (name, street, zip, city, phone, email, url), for example reading your organisation snippet, and set it as company_data_provider. The option company_data puts it into the footer of every page or into the closing box.

Customizing the layout

Override @SuluPdf/document.html.twig with templates/bundles/SuluPdfBundle/document.html.twig. dompdf understands CSS 2.1 only (no flexbox, grid, custom properties), so the layout is plain on purpose.

Good to know

  • The selectors depend on your theme's markup - set the rules per project.
  • Images are fetched over HTTP; on local hosts (localhost, local.*, *.test, *.local) certificate verification is switched off for that.
  • .webp images are requested as .jpg (dompdf cannot read WebP; Sulu picks the format by URL extension).
  • The logo must be PNG or JPG.
  • Fonts are registered in %kernel.cache_dir%/sulu_pdf on the first request after a cache clear.

Tests

composer install && vendor/bin/phpunit

License

MIT