Skip to content

feat: handout export (slides + notes + cover/footer/ending)#2278

Open
oripka wants to merge 9 commits into
slidevjs:mainfrom
oripka:feat-handout
Open

feat: handout export (slides + notes + cover/footer/ending)#2278
oripka wants to merge 9 commits into
slidevjs:mainfrom
oripka:feat-handout

Conversation

@oripka
Copy link
Copy Markdown
Contributor

@oripka oripka commented Sep 16, 2025

Introduces a dedicated handout export that renders one A4 page per slide with:

  • a scaled slide preview,
  • speaker notes,
  • and a customizable footer.

Optionally, a multi-page cover can be prepended.
Export is supported both in the CLI and the browser exporter, designed for reliable print output.

Handout rendering and routes

  • New print routes for handout and cover, enabled whenever print or browser exporter features are active.

    • packages/client/setup/routes.ts
    • packages/client/pages/handout/print.vue
    • packages/client/pages/cover/print.vue

Page layout

  • Each page: slide (top), notes (middle), footer (bottom).

  • Uses fixed A4 dimensions in CSS pixels (96 DPI).

  • Enforces print media styles for consistency.

    • packages/client/internals/PrintHandout.vue
    • packages/client/internals/PrintContainerHandout.vue

Virtual global components (user-overridable)

  • Auto-discovers handout-cover.vue / HandoutCover.vue and handout-bottom.vue / HandoutBottom.vue.

  • Falls back to no-op components if not present.

  • Props like pageNumber are forwarded (supports camel/kebab case).

    • packages/slidev/node/virtual/handout-components.ts
    • packages/slidev/node/virtual/index.ts

CLI integration

  • slidev export --handout<output>-handout.pdf.

  • --cover prepends cover pages if defined.

  • --range limits exported slides.

  • Uses fixed A4 viewport and print media for stability.

    • packages/slidev/node/commands/export.ts
    • packages/slidev/node/cli.ts
    • packages/types/src/cli.ts

Print safety

  • Disables slide-sized @page rules for handout/cover routes so A4 applies cleanly.

    • packages/client/composables/usePrintStyles.ts

Navigation updates

  • isPrintMode now includes handout and cover.

    • packages/client/composables/useNav.ts

Starter examples

  • Demo components provided:

    • demo/starter/handout-cover.vue
    • demo/starter/handout-bottom.vue

Usage

CLI

# Regular slides export (unchanged)
slidev export --output slides.pdf

# Handout export
slidev export --handout --output slides.pdf

# Handout with cover
slidev export --handout --cover --output slides.pdf

# With range
slidev export --handout --range 1,3-5 --output slides.pdf

👉 Produces slides-handout.pdf.

Browser

  • Run dev server.
  • Open /handout?print, /handout?cover, or /handout?range=1,3-5.
  • Print to PDF (set paper size = A4, enable background graphics).

Customization

  • Cover: add handout-cover.vue for one or more cover pages. Use .break-after-page to force page breaks.
  • Footer: add handout-bottom.vue to customize footer content (e.g. company name, year). Receives pageNumber prop.
  • Footer layout reserves space; top rule is drawn by the container. Page numbering offsets correctly when cover pages exist.

Implementation Notes

  • Fixed A4 viewport + print media reduce risk of black/blank pages during export.
  • Uses preferCSSPageSize, zero margins, and light color scheme.
  • Cover page offset computed by counting .break-after-page elements.

@netlify
Copy link
Copy Markdown

netlify Bot commented Sep 16, 2025

Deploy Preview for slidev failed.

Name Link
🔨 Latest commit 5c41584
🔍 Latest deploy log https://app.netlify.com/projects/slidev/deploys/69d6aeb4c9d8ca000816ee47

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Sep 16, 2025

Open in StackBlitz

@slidev/client

npm i https://pkg.pr.new/@slidev/client@2278

create-slidev

npm i https://pkg.pr.new/create-slidev@2278

create-slidev-theme

npm i https://pkg.pr.new/create-slidev-theme@2278

@slidev/parser

npm i https://pkg.pr.new/@slidev/parser@2278

@slidev/cli

npm i https://pkg.pr.new/@slidev/cli@2278

@slidev/types

npm i https://pkg.pr.new/@slidev/types@2278

commit: 5c41584

Comment thread packages/types/src/cli.ts Outdated
export interface ExportArgs extends CommonArgs {
'output'?: string
'handout'?: boolean
'cover'?: boolean
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not so sure about the cover part, as it doesn't really feel like to be part of Slidev, and I am concern if we do we would need to handle request of mutli-pages cover or the ending page etc. As people can always edit the PDF to prepend or append pages, I don't think it's a must-have right now. Can we leave it to be discussed the future?

Copy link
Copy Markdown
Contributor Author

@oripka oripka Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, the cover page is an essential use case. One of the great things I love about Slidev—and the main reason I moved all my slides from PowerPoint—is that it allows me to keep consistent layouts while easily changing branding and slide styles, all without touching the content.

For example, I have training materials written in Slidev. When I deliver a training for Client A, the slides need different logos and branding than for Client B. Achieving this with PowerPoint and master slides is a pain and very difficult to keep in sync. With Slidev, it’s straightforward thanks to custom layouts and components.

Using Slidev for delivering courses is, I believe, a common use case. For students, it’s also standard to receive a handout with the speaker notes after the training (this is an industry standard and a core feature of competing software such as PowerPoint). To make these handouts look professional, a cover page is necessary—because the output then essentially becomes a book. That’s why I see this as a feature that aligns very well with Slidev.

I understand the hesitation to integrate a feature that, at first glance, might seem unrelated to presentation software. And yes, one could implement it externally. But doing so makes certain things very difficult, such as:
• having consistent page numbers on the cover pages, content pages and ending pages
• ensuring consistent layout across cover pages, ending pages, and content,
• applying a common footer.

An external solution would create repetition and friction. Since Slidev already has much of the necessary infrastructure and code in place, I believe this is a natural fit.

In my recent commits, I’ve added support for ending pages, additional output formats, and made some documentation enhancements and cleanups.

@oripka oripka changed the title feat: A4 handout export (slides + notes + cover/footer) feat: handout export (slides + notes + cover/footer/ending) Sep 23, 2025
Copy link
Copy Markdown
Member

@antfu antfu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work! I'd love to have it, let's move it forward.

A few suggestions:

Comment thread demo/starter/handout-cover.vue Outdated
</h1>
<p class="mt-6 max-w-xl text-lg leading-relaxed text-slate-700">
A companion handout for the demo deck that showcases Slidev's text-first authoring, navigation helpers, and Vue-driven
enhancements. Explore the pages ahead to follow along without missing any of the live interactions.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is the starter, I wanted to keep it simple. Could you reduce the content of handing-cover and handout-ending to only keep the minimal content for demonstration.

Comment thread docs/builtin/cli.md Outdated
Comment on lines +73 to +74
- `--cover` (`boolean`, default: `false`): prepend global cover pages from `handout-cover.vue` when handouts are exported.
- `--ending` (`boolean`, default: `false`): append closing pages from `handout-ending.vue` when handouts are exported.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove these two flags; we could auto-apply when the pages exist. I believe it's not a common need to toggle them through the cli flag.

</div>
</template>

<style scoped>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid using scoped style, but use a .slidev- prefixed CSS in the style.css, which would allow user to override

Comment thread packages/client/pages/cover/print.vue Outdated
</template>

<style>
html.print,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we are duplicating the styles and logics here?

Comment thread packages/client/setup/routes.ts Outdated
},
{
name: 'cover',
path: '/cover',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do /handout/cover or /handout-cover?

Comment thread packages/slidev/node/cli.ts Outdated
Comment on lines +42 to +47
'handout-bottom.vue',
'handout-cover.vue',
'handout-ending.vue',
'HandoutBottom.vue',
'HandoutCover.vue',
'HandoutEnding.vue',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'handout-bottom.vue',
'handout-cover.vue',
'handout-ending.vue',
'HandoutBottom.vue',
'HandoutCover.vue',
'HandoutEnding.vue',
'handout-bottom.vue',
'handout-cover.vue',
'handout-ending.vue',

Let's keep it simple and consistent

Comment thread packages/slidev/node/cli.ts Outdated
Comment on lines +593 to +604
.option('handout', {
type: 'boolean',
describe: 'export handout PDF (configurable page size, one page per slide with notes and header/footer) to a separate file',
})
.option('cover', {
type: 'boolean',
describe: 'prepend handout cover page(s) if available (requires handout-cover.vue)',
})
.option('ending', {
type: 'boolean',
describe: 'append handout ending page(s) if available (requires handout-ending.vue)',
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.option('handout', {
type: 'boolean',
describe: 'export handout PDF (configurable page size, one page per slide with notes and header/footer) to a separate file',
})
.option('cover', {
type: 'boolean',
describe: 'prepend handout cover page(s) if available (requires handout-cover.vue)',
})
.option('ending', {
type: 'boolean',
describe: 'append handout ending page(s) if available (requires handout-ending.vue)',
})
.option('handout', {
type: 'boolean',
describe: 'export handout PDF (configurable page size, one page per slide with notes and header/footer) to a separate file',
})

@oripka
Copy link
Copy Markdown
Contributor Author

oripka commented Mar 4, 2026

Thanks again for the detailed review. I rebased the branch on latest main and force-pushed.

  1. Minimal starter examples for cover/ending
  • demo/starter/handout-cover.vue
  • demo/starter/handout-ending.vue
  1. Remove cover / ending CLI flags
  • packages/slidev/node/cli.ts
  • packages/types/src/cli.ts
  • docs/builtin/cli.md
  1. Auto-apply cover/ending when files exist
  • packages/slidev/node/commands/export.ts
    • resolveHandoutPageInclusion(...)
    • handout export query now auto-adds cover / ending based on file presence
  1. Avoid scoped style; use overridable prefixed CSS
  • packages/client/internals/PrintHandout.vue (template/class usage)
  • packages/client/styles/index.css (.slidev-handout-* rules)
  1. Remove duplicated cover page route/logic
  • removed packages/client/pages/cover/print.vue
  • packages/client/setup/routes.ts now keeps only /handout
  • related print-mode checks updated in useNav.ts and usePrintStyles.ts
  1. Keep naming simple/consistent (lowercase only)
  • packages/slidev/node/virtual/handout-components.ts
  • restart watch list in packages/slidev/node/cli.ts uses lowercase handout filenames

Also added focused tests:

  • test/handout-config.test.ts
  • test/handout-export-options.test.ts

All PR checks are green now.

@oripka oripka requested a review from antfu March 4, 2026 14:52
@kermanx
Copy link
Copy Markdown
Member

kermanx commented Mar 19, 2026

I personally think it could be better if we could support any custom print template, just like #1513? Then the handout export can be implemented as a built-in print template, alongside the default one.

@jzarzeckis
Copy link
Copy Markdown

I'm not really sure there are a lot of users or use-cases demanding custom print templates.

Would be very happy to see this "opinionated" option merged, and use this instead :)

Copy link
Copy Markdown

@jzarzeckis jzarzeckis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw - I just tested this with a presentation of mine. And I noticed a few things that are broken in the generated handout:

  • If the presentation uses dark theme - handout gets light text on dark background.
  • Weird spacing of presenter notes (the presenter notes look fine in the PDF I can generate with slidev export-notes and don't have such weird spacing)
  • The v-mark directive highlights seem broken.

Attaching my exported PDF:

slides-export-handout.pdf

@oripka
Copy link
Copy Markdown
Contributor Author

oripka commented Apr 8, 2026

Btw - I just tested this with a presentation of mine. And I noticed a few things that are broken in the generated handout:

  • If the presentation uses dark theme - handout gets light text on dark background.
  • Weird spacing of presenter notes (the presenter notes look fine in the PDF I can generate with slidev export-notes and don't have such weird spacing)
  • The v-mark directive highlights seem broken.

Attaching my exported PDF:

slides-export-handout.pdf

Do you have a minimal reproduction?

@jzarzeckis
Copy link
Copy Markdown

https://github.com/jzarzeckis/operations-optimizations-vision-slides
Invited you as collaborator, if you care to clone it.

But I believe minimal reproduction shouldn't be too hard - just: use dark theme, add some v-mark directives to several elements in the presentation, add some more extended markdown content to the presenter notes with bulleted lists, tables, etc..

@jzarzeckis
Copy link
Copy Markdown

For reference the expected behavior - how the slidev export-notes works for me:
slides-export-notes.pdf

@oripka
Copy link
Copy Markdown
Contributor Author

oripka commented Apr 8, 2026

Tracked this down and pushed a fix on feat-handout.

What was fixed:

  • handout notes no longer inherit dark-theme prose colors on light paper
  • handout note typography/spacing was normalized so it matches notes export much more closely
  • v-mark / rough-notation alignment in handout export was corrected by propagating the slide scale through the handout wrapper
  • handout export now waits for the handout DOM to fully settle before printing, which avoids broken blank/tiny PDFs and slide-loading races

I also added an opt-in setting for very long notes:

  • handout.paginateOverflow: true

That keeps the default handout path stable, while allowing DOM-based continuation-page pagination when a deck explicitly wants it.

CI is failing now. I will fix that later...

@oripka
Copy link
Copy Markdown
Contributor Author

oripka commented Apr 8, 2026

Actually upstream CI is broken 😅 so I will wait until the maintainers have fixed this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants