Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions demo/starter/handout-bottom.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
defineProps<{ pageNumber: number }>()
// A footer layout that relies on the parent handout container for the full-width
// top rule and page number placement. Avoid absolute positioning so the
// container can reserve space and prevent overlaps.
const year = new Date().getFullYear()
// Replace these with your flavor system if needed
const company = 'Slidev'
const rightText = 'Professional courses for developers'
const series = 'Engineering Enablement Series'
</script>

<template>
<div class="handout-footer">
<div class="handout-row">
<div class="left">
© {{ year }} {{ company }}
</div>
<div class="center">
Page {{ pageNumber }}
</div>
<div class="right">
<span class="block font-semibold uppercase tracking-wide text-xs text-gray-500">
{{ series }}
</span>
<span>{{ rightText }}</span>
</div>
</div>
</div>
</template>

<style scoped>
.handout-footer {
width: 100%;
}
.handout-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
font-size: 11px;
line-height: 1.2;
padding-top: 3mm; /* space below the rule drawn by container */
}
.left,
.center,
.right {
white-space: nowrap;
}
.center {
font-variant-numeric: tabular-nums;
color: rgba(0, 0, 0, 0.7);
}
.right {
text-align: right;
}
</style>
86 changes: 86 additions & 0 deletions demo/starter/handout-cover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<div>
<div
data-handout-page
class="break-after-page h-[270mm] px-16 py-20 flex flex-col justify-between text-slate-900 bg-gradient-to-br from-slate-50 via-white to-slate-100"
>
<div>
<span class="tracking-[0.35em] text-xs font-semibold uppercase text-sky-600">
Slidev Starter Template
</span>
<h1 class="mt-6 text-6xl font-bold border-b-4 border-slate-900 inline-block pr-4">
Welcome to Slidev
</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.
</p>
<div class="mt-10 flex flex-wrap gap-3 text-xs font-semibold uppercase tracking-[0.3em] text-slate-500">
<span class="bg-white/80 px-3 py-2 rounded-full border border-slate-200">Markdown Workflow</span>
<span class="bg-white/80 px-3 py-2 rounded-full border border-slate-200">Developer Features</span>
<span class="bg-white/80 px-3 py-2 rounded-full border border-slate-200">Interactive Demos</span>
</div>
</div>
<div class="flex justify-between text-sm font-semibold uppercase tracking-wide text-slate-500">
<span>{{ new Date().toLocaleDateString() }}</span>
<span>Prepared by the Slidev Team</span>
</div>
</div>
<div
data-handout-page
class="break-after-page px-16 py-20 text-slate-800 leading-relaxed"
>
<h2 class="text-2xl font-bold tracking-tight text-slate-900">
Inside this deck
</h2>
<p class="mt-4">
The slides introduce Slidev, how to navigate a live presentation, and the built-in developer tooling that powers code-focused
talks. Use this page as a quick reference for the sections you will encounter.
</p>
<div class="grid grid-cols-2 gap-10 mt-10 text-sm">
<div>
<h3 class="text-xs font-semibold uppercase tracking-widest text-slate-500">
Highlights
</h3>
<ul class="mt-3 space-y-2">
<li>• Welcome tour of the starter theme</li>
<li>• Why Slidev embraces a text-based workflow</li>
<li>• Navigation tips and keyboard shortcuts</li>
<li>• Code demos with live annotations</li>
</ul>
</div>
<div>
<h3 class="text-xs font-semibold uppercase tracking-widest text-slate-500">
Keep an eye out for
</h3>
<ul class="mt-3 space-y-2">
<li>• Embedded <code>&lt;Toc&gt;</code> components for structure</li>
<li>• Magic Move transitions between code samples</li>
<li>• UnoCSS utilities applied per-slide</li>
<li>• Links to deeper documentation throughout</li>
</ul>
</div>
</div>
</div>
<div
data-handout-page
class="px-16 py-20 text-slate-700 leading-relaxed"
>
<h2 class="text-2xl font-bold tracking-tight text-slate-900">
Adapting the starter
</h2>
<p class="mt-4 text-sm">
Reuse the patterns demonstrated in this deck when crafting your own Slidev presentations. The guidelines below mirror the
recommendations sprinkled across the slides.
</p>
<ol class="mt-6 space-y-3 text-sm list-decimal list-inside">
<li>Begin with Markdown content, then enhance with Vue components only where interactivity adds clarity.</li>
<li>Leverage theme tokens and utilities for consistent typography across slides and handouts.</li>
<li>Document keyboard shortcuts and navigation cues so audiences can revisit the material asynchronously.</li>
</ol>
<p class="mt-6 text-xs uppercase tracking-widest text-slate-500">
Questions? Join the community on Discord or visit sli.dev for more examples.
</p>
</div>
</div>
</template>
71 changes: 71 additions & 0 deletions demo/starter/handout-ending.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<template>
<div class="text-slate-800">
<div
data-handout-page
class="break-after-page px-16 py-20 leading-relaxed bg-white"
>
<h2 class="text-3xl font-bold tracking-tight text-slate-900">
Keep exploring Slidev
</h2>
<p class="mt-6 text-sm max-w-2xl">
This demo highlighted Slidev's text-centric authoring, navigation aids, and live coding capabilities. Use the prompts below
to turn the starter deck into your own developer-friendly presentations.
</p>
<div class="mt-12 grid grid-cols-2 gap-10 text-sm">
<div>
<h3 class="uppercase text-xs tracking-widest font-semibold text-slate-500">
Try it yourself
</h3>
<ol class="mt-3 space-y-2 list-decimal list-inside">
<li>Run <code>pnpm dev</code> and explore the navigation drawer and presenter mode.</li>
<li>Swap the welcome slide content and background image to match your story.</li>
<li>Embed a Vue component or external code sample to showcase interactive docs.</li>
</ol>
</div>
<div>
<h3 class="uppercase text-xs tracking-widest font-semibold text-slate-500">
Enhance your deck
</h3>
<ul class="mt-3 space-y-2">
<li>• Document useful keyboard shortcuts in early slides.</li>
<li>• Use the <code>&lt;Toc&gt;</code> component to keep longer talks scannable.</li>
<li>• Apply UnoCSS utilities or scoped styles to reinforce your brand.</li>
</ul>
</div>
</div>
</div>
<div
data-handout-page
class="px-16 py-20 bg-white leading-relaxed"
>
<h2 class="text-2xl font-semibold tracking-tight text-slate-900">
Resources &amp; community
</h2>
<p class="mt-4 text-sm">
Continue learning with the official guides and community spaces below. They mirror the references called out across the
slides and help you dive deeper into advanced workflows.
</p>
<div class="mt-8 space-y-4 text-sm">
<div>
<h3 class="font-semibold text-slate-600">
Documentation quick hits
</h3>
<ul class="mt-2 space-y-1">
<li><strong>sli.dev/guide/why</strong> — Understand the philosophy behind text-first slides</li>
<li><strong>sli.dev/guide/ui</strong> — Learn every navigation control shown in the demo</li>
<li><strong>sli.dev/features/line-highlighting</strong> — Recreate the code walkthrough effects</li>
</ul>
</div>
<div>
<h3 class="font-semibold text-slate-600">
Stay in touch
</h3>
<p class="mt-2">
Join the community on <strong>Discord</strong>, follow <strong>@slidevjs</strong> for release news, or browse the
theme gallery for inspiration. Share what you build — we love seeing Slidev in the wild!
</p>
</div>
</div>
</div>
</div>
</template>
9 changes: 9 additions & 0 deletions docs/builtin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,22 @@ Export slides to PDF (or other format). See <LinkInline link="guide/exporting" /
Options:

- `--output` (`string`, default: use `exportFilename` (see https://sli.dev/custom/#frontmatter-configures) or use `[entry]-export`): path to the output.
- `--handout` (`boolean`, default: `false`): generate a companion handout PDF (notes + footer) using the configured paper size.
- `--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.
- `--format` (`'pdf', 'png', 'pptx', 'md'`, default: `'pdf'`): output format.
- `--timeout` (`number`, default: `30000`): timeout for rendering the print page (see https://playwright.dev/docs/api/class-page#page-goto).
- `--wait` (`number`, default: `0`): wait the specified milliseconds before capturing each slide.
- `--wait-until` (`'networkidle', 'load', 'domcontentloaded', 'none'`, default: `'networkidle'`): wait for a specific Playwright event before each capture.
- `--range` (`string`): page ranges to export (example: `'1,4-5,6'`).
- `--dark` (`boolean`, default: `false`): export as dark theme.
- `--with-clicks`, `-c` (`boolean`, default: `false`): export pages for every click animation (see https://sli.dev/guide/animations.html#click-animation).
- `--theme`, `-t` (`string`): override theme.
- `--executable-path` (`string`): provide a custom Chromium/Chrome executable for Playwright.
- `--omit-background` (`boolean`, default: `false`): remove the default browser background
- `--with-toc` (`boolean`, default: `false`): generate a PDF outline when exporting to PDF.
- `--per-slide` (`boolean`, default: `false`): render each slide in isolation. Useful for heavy global components.
- `--scale` (`number`, default: `2`): device scale factor for non-handout exports.

## `slidev format [entry]` {#format}

Expand Down
14 changes: 14 additions & 0 deletions docs/custom/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ export:
dark: false
withClicks: false
withToc: false
# handout export presets (paper size, margins, cover margins)
handout:
size: A4
orientation: portrait
margins:
top: 0cm
bottom: 0cm
left: 0cm
right: 0cm
coverMargins:
top: 1cm
bottom: 1cm
left: 1.5cm
right: 1.5cm
# enable twoslash, can be boolean, 'dev' or 'build'
twoslash: true
# show line numbers in code blocks
Expand Down
32 changes: 32 additions & 0 deletions docs/guide/exporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,38 @@ Chromium may miss some features like codecs that are required to decode some vid
$ slidev export --executable-path [path_to_chromium]
```

### Handout exports

Use the `--handout` flag to render an A4 (or customised) PDF that places each slide above its speaker notes and a shared footer component. Slidev writes this PDF alongside your primary export using the suffix `-handout.pdf`:

```bash
$ slidev export --handout
```

You can prepend multi-page covers with `--cover` and append closing/appendix pages with `--ending`. Both switches look for global Vue components in the project root (`handout-cover.vue` and `handout-ending.vue`). Any element with the UnoCSS utility `break-after-page` (or a `data-handout-page` attribute) forces an additional printed page, making it easy to create multi-page introductions or wrap-up sections.

Paper size and margins are configurable from the deck headmatter:

```yaml
---
handout:
size: letter # or A4, Legal, Tabloid …
orientation: landscape
margins:
top: 0.5in
bottom: 0.5in
left: 0.75in
right: 0.75in
coverMargins:
top: 1cm
bottom: 1cm
left: 1.5cm
right: 1.5cm
---
```

The `size` option accepts common international formats (A3, A4, A5) and North American sizes (Letter, Legal, Tabloid, Executive). Provide both `width` and `height` to specify a custom paper size.

### PDF Outline

> Available since v0.36.10
Expand Down
2 changes: 1 addition & 1 deletion packages/client/composables/useNav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
router?.currentRoute?.value?.query
return new URLSearchParams(location.search)
})
const isPrintMode = computed(() => query.value.has('print') || currentRoute.name === 'export')
const isPrintMode = computed(() => query.value.has('print') || currentRoute.name === 'export' || currentRoute.name === 'handout' || currentRoute.name === 'cover')
const isPrintWithClicks = ref(query.value.get('print') === 'clicks')
const isEmbedded = computed(() => query.value.has('embedded'))
const isPlaying = computed(() => currentRoute.name === 'play')
Expand Down
43 changes: 41 additions & 2 deletions packages/client/composables/usePrintStyles.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { useStyleTag } from '@vueuse/core'
import { computed } from 'vue'
import { slideHeight, slideWidth } from '../env'
import { useRoute } from 'vue-router'
import { configs, slideHeight, slideWidth } from '../env'
import { useNav } from './useNav'

export function usePrintStyles() {
const { isPrintMode } = useNav()
const route = useRoute()

useStyleTag(computed(() => isPrintMode.value
// Only inject slide-sized @page for the default print/export view.
// Handout and cover have their own A4 page sizing and should not be overridden.
useStyleTag(computed(() => (isPrintMode.value && !['handout', 'cover'].includes((route.name as string) || ''))
? `
@page {
size: ${slideWidth.value}px ${slideHeight.value}px;
Expand All @@ -26,3 +30,38 @@ export function patchMonacoColors() {
el.media = ''
})
}

export type HandoutPageContext = 'handout' | 'cover' | 'ending'

export function useHandoutPageSetup(context: HandoutPageContext = 'handout') {
const { isPrintMode } = useNav()
const route = useRoute()

const allowedRoute = computed(() => {
if (context === 'cover')
return route.name === 'cover'
return route.name === 'handout'
})

const margins = computed(() => context === 'cover'
? configs.handout.coverMargins
: configs.handout.margins)
const pageSize = computed(() => configs.handout.cssPageSize)

useStyleTag(computed(() => (isPrintMode.value && allowedRoute.value)
? `
@page {
size: ${pageSize.value};
margin-top: ${margins.value.top};
margin-right: ${margins.value.right};
margin-bottom: ${margins.value.bottom};
margin-left: ${margins.value.left};
}
`
: ''))

return {
margins,
pageSize,
}
}
Loading
Loading