CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

jbaruch/speaker-toolkit

Eleven skills for conference speakers covering rhetoric analysis, talk authoring, deck building and export, CFP submissions, publishing, illustrations, shownotes, and screencast production, informed by a 113-entry Presentation Patterns taxonomy (83 observable: 64 patterns + 19 antipatterns; 30 unobservable: 21 patterns + 9 antipatterns).

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

template-conditionals.mdskills/shownotes-publisher/references/

Template Conditionals — What talk.html Renders

~/Projects/shownotes/_layouts/talk.html is the talk page layout. It reads the extracted_* fields the parser populates and decides what to render. This file maps each conditional to the source field that controls it, so an author knows exactly which extracted field they need to populate (or leave empty) to produce a given visual outcome.

Title Block

{% assign display_title = page.extracted_title | default: page.title %}
{% if page.extracted_title %}
<h1 class="talk-title">{{ display_title }}</h1>
{% else %}
<h1 class="talk-title">{{ display_title | replace: "-", " " | capitalize }}</h1>
{% endif %}
  • If extracted_title is set → renders the H1 as-is
  • Else falls back to humanizing the slugified filename (replaces dashes with spaces and capitalizes)

The fallback is the parser's safety net — but never relying on it means: always include # {Talk Title} as the first H1 in the body.

Conference Name

{% if page.extracted_conference %}
  <span class="meta-item conference-name">{{ page.extracted_conference }}</span>
{% elsif page.conference %}
  <span class="meta-item conference-name">{{ page.conference }}</span>
{% endif %}
  • extracted_conference from the body's **Conference:** value line is preferred
  • Frontmatter conference: is a legacy fallback the layout supports but the parser doesn't populate
  • If neither is set, no conference name renders

Date

Same pattern as conference: extracted_date from body, then frontmatter date:, then nothing. The layout applies date_to_xmlschema and formats with %B %d, %Y — so an ISO YYYY-MM-DD value renders as "May 14, 2026".

Video Status Badge — the conditional that matters most

{% if page.extracted_video %}
<span class="meta-item status-badge video-published">Video Available</span>
{% else %}
<span class="meta-item status-badge video-pending">Video Coming Soon</span>
{% endif %}

This is the source of the "don't add video until it's available" rule. Three states:

Body has **Video:** line?extracted_videoBadgeVideo embed renders?
Absentnil (falsy)"Video Coming Soon"No
Present with valid URLURL string (truthy)"Video Available"Yes
Present with bare-text placeholder (TBD, Coming soon)the text string (still truthy)"Video Available"Yes — embed broken (no valid URL extracted)

The "Video Available" badge fires on ANY truthy extracted_video — the layout doesn't validate that the value is a real URL. A placeholder like **Video:** TBD will mark the talk "Available" and attempt to embed TBD as a video. Both wrong.

The only correct way to express "video not yet available" is to omit the line entirely.

Media Section (Slides + Video Embeds)

{% if page.extracted_slides or page.extracted_video %}
<section class="talk-main-content">
  <div class="media-container">
    {% if page.extracted_slides %}<div class="media-item slides-embed">…</div>{% endif %}
    {% if page.extracted_video %}<div class="media-item video-embed">…</div>{% endif %}
  </div>
</section>
{% endif %}
  • If neither slides nor video is set → no media section renders
  • If only slides → just slides (typical for pre-talk publish)
  • If only video → just video (rare; usually means slides URL is broken)
  • Both → side-by-side embed

The embed itself is handled by _includes/embedded_resource.html, which pattern-matches the URL to decide the embed format:

  • docs.google.com/presentation/d/ → Google Slides iframe
  • drive.google.com/file/d/ → Google Drive PDF preview (with fallback when blocked by content extensions)
  • youtube.com/watch?v= or youtu.be/ → YouTube iframe

A URL that doesn't match any pattern produces a broken embed. Use the documented forms: Google Drive /preview for slides PDFs, full YouTube watch URLs for videos.

Abstract

{% if page.extracted_abstract %}
<div class="talk-abstract">
  <h2>Abstract</h2>
  {{ page.extracted_abstract | markdownify }}
</div>
{% endif %}

Markdownified at the very end. By the time markdownify runs, the extracted_abstract value is ONE STRING — the parser already joined lines with spaces. So markdown structure that depends on line breaks (headings, lists, code blocks) is gone before markdownify sees it.

This is why "abstract is one paragraph" is the strict rule.

Resources

{% if page.extracted_resources and page.extracted_resources != "" %}
<div class="talk-resources">
  <h2>Resources</h2>
  <div class="resources-list">{{ page.extracted_resources | markdownify }}</div>
</div>
{% endif %}

Markdownified verbatim — line breaks preserved. Full markdown works here.

Presentation Context

{% if page.extracted_presentation_context %}
<div class="talk-presentation-context">
  {{ page.extracted_presentation_context }}
</div>
{% endif %}

NOT markdownified — rendered as-is. The parser has already processed liquid template variables (e.g., the speaker name interpolation), so the output is plain text with whatever HTML the source contained. Don't include markdown syntax expecting it to render.

Thumbnail Resolution — Filename Convention, Not Frontmatter

Thumbnails are NOT controlled by frontmatter. The path is computed from the talk's .md filename in three independent places:

index.md (Highlighted Presentations grid):

{% assign talk_slug = talk.path | split: '/' | last | replace: '.md', '' %}
{% assign thumbnail_path = '/assets/images/thumbnails/' | append: talk_slug | append: '-thumbnail.png' %}

_layouts/default.html (OG + Twitter card meta tags):

<meta property="og:image" content="{{ '/assets/images/thumbnails/' | append: talk_slug | append: '-thumbnail.png' | absolute_url }}">
<meta name="twitter:image" content="{{ '/assets/images/thumbnails/' | append: talk_slug | append: '-thumbnail.png' | absolute_url }}">

_includes/embedded_resource.html (slides/video preview thumbs):

{% assign local_thumbnail_path = "/assets/images/thumbnails/" | append: talk_slug | append: "-thumbnail.png" %}
{% assign thumb_url = local_thumbnail_path | relative_url %}

All three derive {slug} from the .md filename, append -thumbnail.png, and prefix /assets/images/thumbnails/. No frontmatter field — thumbnail_url: or otherwise — overrides this.

What thumbnail_url: in frontmatter actually does

Exactly one thing: index.md:104 checks {% if talk.extracted_slides or talk.extracted_video or talk.thumbnail_url %} to decide whether to include the talk in the homepage's "Highlighted Presentations" featured set. The check is for truthiness only — the value is never read for a URL. In practice the check is redundant: extracted_slides is almost always populated from the body field block, so the frontmatter field rarely affects the outcome.

Setting thumbnail_url: to a non-existent path doesn't make it exist; setting it to a different path doesn't override the slug-derived path; omitting it doesn't disable the thumbnail.

Missing-thumbnail fallback

Every <img> tag that points to the slug-derived thumbnail has an onerror handler that swaps in the placeholder SVG:

<img src="{{ thumb_url }}" ...
     data-fallback="{{ placeholder_url }}"
     onerror="this.onerror=null;this.src=this.dataset.fallback;">

So a talk without a thumbnail file at the conventional path still renders the page correctly — the browser fires onerror on the 404 and substitutes /assets/images/placeholder-thumbnail.svg.

Thumbnail Display Geometry

The referenced site's assets/css/main.css uses a 16:9 featured-thumbnail container and 160×90 list thumbnails, both with object-fit: cover. Its mobile list thumbnails are 64×36. These are CSS display sizes, not source-image conversion requirements; preserve the illustrations owner's source dimensions. The Step 6 placement owner validates or converts its PNG/JPEG result to PNG for the site's fixed filename without changing the source. Existing thumbnails retain their legacy dimensions; the CSS handles their display. The templates and social-image metadata use the same convention-path asset.

Source: shownotes CSS at a69a4b3. Inspect the destination's current template and CSS during site validation if its theme differs; verify that the title and face remain visible in the previews.

Structured Data (JSON-LD)

The layout emits a JSON-LD PresentationDigitalDocument block using:

  • name: extracted_title (or page.title fallback)
  • author.name: site.speaker.display_name (or site.speaker.name)
  • publisher.name: extracted_conference
  • datePublished: extracted_date
  • description: extracted_abstract (when set)
  • url: absolute URL of the page

These are all derived — no extra authoring step needed. If a field is missing, the JSON-LD entry for it is missing too, which degrades SEO but doesn't break the page.

Agent Skill

When page.skill exists, talk.html renders a Skill Available badge and includes skill_section.html beneath the media section. The include shows the skill name, description, install command, raw download link, and collapsed body. Without a matching skill file, the entire section and badge are absent. See talk-skill.md for source paths and verification.

skills

README.md

tile.json