Skip to main content
Write speaker notes in markdown and display them during your presentation. Notes are parsed from a markdown file where each section corresponds to a slide.

Format

Speaker notes use a simple markdown format: one section per slide, separated by --- on its own line. Sections are matched to slides by position:
  • First section → Slide 1
  • Second section → Slide 2
  • Empty sections → No notes for that slide
notes.md
Welcome everyone. This is the opening slide.

---

Talk about the base container here.

---

---

Slide 4 notes. Slide 3 had none.
Keep the number of sections in sync with your slides. If you have 12 slides, you need 12 sections (empty sections are fine).

Parsing Notes

Use parseSpeakerNotes() to convert the markdown file into an array that matches your slides:
app/slides/layout.tsx
import fs from 'fs';
import path from 'path';
import { SlideDeck, parseSpeakerNotes } from 'nextjs-slides';
import { slides } from './slides';

const notes = parseSpeakerNotes(
  fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8')
);

export default function SlidesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <SlideDeck
      slides={slides}
      speakerNotes={notes}
      syncEndpoint="/api/nxs-sync"
    >
      {children}
    </SlideDeck>
  );
}
The function returns (string | null)[] where:
  • notes[0] corresponds to slide 1
  • notes[1] corresponds to slide 2
  • null entries indicate no notes for that slide

Stripping Leading Titles

If your notes file starts with a document title (a single markdown heading), use stripLeadingTitle: true to prevent it from being treated as slide 1:
notes.md
# My Presentation Notes

This is slide 1 content.

---

This is slide 2 content.
const notes = parseSpeakerNotes(
  fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8'),
  { stripLeadingTitle: true }
);
Without this option, ”# My Presentation Notes” would be treated as the notes for slide 1.

Section Separators

The --- separator must be on its own line:
First slide notes.

---

Second slide notes.
The parser splits on /^---$/m (a line containing only ---), so surrounding whitespace is trimmed but the separator needs its own line.

Demo Notes

Add extra sections after your last slide to create demo notes. These appear after the presentation ends and let you step through talking points for live demos:
notes.md
...last slide notes

---

Open the counter demo. Show how useState drives the count.

---

Switch to the editor. Walk through adding a new slide.
During the presentation, the phone auto-syncs to the current slide. Once you navigate past the last slide, the header switches to “Demo 1 / 2” and you manually control the demo notes.
See Phone Sync for details on setting up the sync endpoint and notes viewer.

Implementation Details

The parseSpeakerNotes function is implemented as:
src/parse-speaker-notes.ts
export function parseSpeakerNotes(
  markdown: string,
  options?: { stripLeadingTitle?: boolean }
): (string | null)[] {
  let sections = markdown.split(/^---$/m).map((section) => {
    const trimmed = section.trim();
    return trimmed.length > 0 ? trimmed : null;
  });

  if (
    options?.stripLeadingTitle &&
    sections[0] &&
    /^#+\s+.+$/.test(sections[0].replace(/\n.*/s, ''))
  ) {
    sections = sections.slice(1);
  }

  return sections;
}
It splits the markdown on --- separators, trims whitespace, and optionally removes a leading title section if the first line matches a markdown heading pattern (# Title).

Build docs developers (and LLMs) love