Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a113447
build(deps): bump the rust-host group with 3 updates
dependabot[bot] Oct 14, 2026
5111721
build(deps): bump toml in /src/editors/zed in the rust-zed group
dependabot[bot] Oct 14, 2026
5950d3f
build(deps): clobber-merge dependabot/cargo/src/editors/zed/dependabo…
github-actions[bot] Oct 14, 2026
70e2a3b
build(deps): bump the github-actions group with 6 updates
dependabot[bot] Oct 14, 2026
114737a
build(deps): bump @playwright/test in /src/website in the website group
dependabot[bot] Oct 14, 2026
fe12a1a
build(deps): clobber-merge dependabot/github_actions/dependabot-upgra…
github-actions[bot] Oct 14, 2026
e98ea93
build(deps): clobber-merge dependabot/npm_and_yarn/src/website/depend…
github-actions[bot] Oct 14, 2026
c42ca83
build(deps-dev): bump the vscode-extension group
dependabot[bot] Oct 14, 2026
9929b5f
build(deps): clobber-merge dependabot/npm_and_yarn/src/editors/vscode…
github-actions[bot] Oct 14, 2026
92573de
Bump the dotnet-sidecars group with 8 updates
dependabot[bot] Oct 14, 2026
62966f6
build(deps): clobber-merge dependabot/nuget/dot-config/dotnet/dependa…
github-actions[bot] Oct 14, 2026
daad1eb
Videos: one folder for every film, still and hero
MelbourneDeveloper Oct 20, 2026
86bcfc2
Videos: the figure shortcode names the new video folder
MelbourneDeveloper Oct 20, 2026
f8bce94
Videos: lessons, slide films that teach one feature
MelbourneDeveloper Oct 20, 2026
9344fee
Videos: the camera follows the action
MelbourneDeveloper Oct 20, 2026
a05432a
Videos: one folder per film, and a lesson template
MelbourneDeveloper Oct 20, 2026
c4eb8cb
Videos: restore the Blazor film from main
MelbourneDeveloper Oct 20, 2026
267777a
Videos: a walkthrough template, cut through the camera
MelbourneDeveloper Oct 20, 2026
b3b6623
Refactoring & Code Actions: the docs page becomes a lesson
MelbourneDeveloper Oct 20, 2026
c07387a
VS Code tests: stop taking documentation screenshots
MelbourneDeveloper Oct 20, 2026
b33feba
VS Code tutorial: walkthroughs whose steps tick on real SharpLsp answ…
MelbourneDeveloper Oct 20, 2026
7b1eefc
Videos: the Blazor film, cut through the camera
MelbourneDeveloper Oct 20, 2026
4758360
VS Code tutorial: end-to-end suite in the fsharp chunk (#412)
MelbourneDeveloper Oct 20, 2026
8b0fa43
Refactoring & Code Actions: the lesson film
MelbourneDeveloper Oct 20, 2026
7b80d60
Tutorial: skip the caption track for films published without captions…
MelbourneDeveloper Oct 20, 2026
537fd39
Merge feature/412-vscode-tutorial: interactive VS Code tutorial (#412)
MelbourneDeveloper Oct 20, 2026
fd7ec50
Merge remote-tracking branch 'origin/dependabot-upgrades' into chore/…
MelbourneDeveloper Oct 20, 2026
9d52ba5
Tutorial: a Code Actions step that plays the refactoring lesson from …
MelbourneDeveloper Oct 20, 2026
59798bb
Tutorial: a code action step on the refactoring lesson film (#412)
MelbourneDeveloper Oct 20, 2026
ff09f1a
Tutorial: keep the panel when the first file opens, and green its sui…
MelbourneDeveloper Oct 20, 2026
f1592a9
Videos: one entry point for every film, the profiler included, and a …
MelbourneDeveloper Oct 20, 2026
1c89d21
Videos: the hover film, cut through the camera
MelbourneDeveloper Oct 20, 2026
53ab42f
Videos: chapters a tutorial step can play, and a film shot for the VS…
MelbourneDeveloper Oct 20, 2026
c11f2a1
Tutorial: "Open a C# file", not "an C# file" (#412)
MelbourneDeveloper Oct 20, 2026
687b15e
Videos: every shoot drives its own editor
MelbourneDeveloper Oct 20, 2026
42be497
Videos: the VS Code tutorial's film, one chaptered shot per step
MelbourneDeveloper Oct 20, 2026
4e15ffc
Videos: completions, F# language and getting started cut through the …
MelbourneDeveloper Oct 20, 2026
516edca
Videos: the navigation film, cut through the camera; openFile skips t…
MelbourneDeveloper Oct 20, 2026
04ff4b3
Contributions test: workbench view commands are <view id><suffix>, ex…
MelbourneDeveloper Oct 20, 2026
c99cf83
Videos: openFile compares the active tab's label exactly, no regex
MelbourneDeveloper Oct 20, 2026
ed90e0c
Videos: one timeline script for every stepped page; one settings writer
MelbourneDeveloper Oct 20, 2026
841ad47
Videos: a lesson's cards, notes and subtitles wait for their line; on…
MelbourneDeveloper Oct 20, 2026
2d319c1
Tutorial: MP4 films VS Code can play, opened on a file SharpLsp has l…
MelbourneDeveloper Oct 20, 2026
5cd1c56
install-vsix: replace SharpLsp in every VS Code profile that holds it
MelbourneDeveloper Oct 20, 2026
bd3c9d9
Videos: the refactoring lesson, republished with each item on its line
MelbourneDeveloper Oct 20, 2026
818e9e6
Tutorial panel: a step whose film has no usable URL shows the fallbac…
MelbourneDeveloper Oct 20, 2026
3f5f6ed
Website: a Tutorial docs page that plays the VS Code tutorial's film,…
MelbourneDeveloper Oct 20, 2026
93562cf
F# document symbols select the declaration's name, not its doc comment
MelbourneDeveloper Oct 20, 2026
7949e6f
Tutorial test: Prettier formatting
MelbourneDeveloper Oct 20, 2026
6187c78
F#: a file outside the solution answers from the project that owns it…
MelbourneDeveloper Oct 20, 2026
6b783d3
C#: a file outside the solution answers from the project that owns it…
MelbourneDeveloper Oct 20, 2026
45d6cb6
Tutorial panel: narrated films offer Sound on, since VS Code autoplay…
MelbourneDeveloper Oct 20, 2026
53b0634
VS Code tutorial film: narrated, with original music and captions in …
MelbourneDeveloper Oct 20, 2026
02d4f65
C#: opening a file outside the solution never restores or writes buil…
MelbourneDeveloper Oct 20, 2026
3ac1f96
Cross-language rename test: a failed fixture build reports MSBuild's …
MelbourneDeveloper Oct 20, 2026
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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

SharpLsp is an open-source, editor-agnostic .NET LSP (C# + F#) built in Rust: one server, the complete .NET development experience in every editor. Stack: C#, Rust, TypeScript

All large files like videos MUST go in Git LFS

**Aim #1: Fix the .NET developer experience.**
Match and surpass Visual Studio, Rider, and C# Dev Kit — no proprietary dependencies, licenses, or vendor lock-in.

Expand Down Expand Up @@ -104,8 +106,9 @@ F# ahead of C# on every new feature. F# never takes the back seat.

# Git

> ⛔️ **Don't touch git** unless the user asks or a skill demands it.

> ⛔️ **Don't make git changes like creating new branches** unless the user asks or a skill demands it.
- Commit and push to submodules before commiting and pushing the feature branch
- Commit and push after each major change
- **One feature branch**, never `git worktree`. Work the open one; several open → merge them immediately
- **Never push to `main`.** PR → CI green → squash-merge → delete the branch
- **Own every PR until green**: `gh pr merge --auto --squash`, then fix and push until all checks pass
Expand Down
40 changes: 29 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ _test-tooling:
node --test tools/vsix/chunk-once.test.mjs
@echo "==> Running repo tooling tests..."
node --test tools/netcoredbg/custody.test.mjs tools/make/reinstall-loop.test.mjs tools/make/vsix-rebuild.test.mjs tools/vsix/rebuild-contract.test.mjs tools/vsix/debug-cleanup.test.mjs tools/audit/dotnet-vulnerable.test.mjs \
tools/rider/gradle.test.mjs tools/vsix/resolve-symlink-stubs.test.mjs
tools/rider/gradle.test.mjs tools/vsix/resolve-symlink-stubs.test.mjs tools/make/vsix-profiles.test.mjs

_website-build:
@echo "==> Building website..."
Expand All @@ -807,13 +807,13 @@ _website-dev:

# [WEB-DESIGN-MEDIA] Shoot and publish one website film from the packaged VSIX:
# `make _film FILM=fsharp`, or `PUBLISH=1` to publish the last shoot again.
# The films live in src/website/tools/profiler-film/films; its README says how.
# The films live in src/website/tools/video/films; its README says how.
_film:
@test -n "$(FILM)" || { echo "usage: make _film FILM=<film> [PUBLISH=1]" >&2; exit 1; }
$(if $(PUBLISH),,$(MAKE) _build-vsix VSIX_PREBUILT=1)
@# Install once: npm ci would wipe the voice model kokoro-js caches in node_modules.
test -d src/website/tools/profiler-film/node_modules || npm ci --prefix src/website/tools/profiler-film
node src/website/tools/profiler-film/film.mjs $(FILM) $(if $(PUBLISH),--publish,)
test -d src/website/tools/video/node_modules || npm ci --prefix src/website/tools/video
node src/website/tools/video/film.mjs $(FILM) $(if $(PUBLISH),--publish,)

_test-website:
@echo "==> Running website Playwright tests..."
Expand All @@ -839,6 +839,8 @@ _lint-zed:
_lint-vsix: _check-vsix-chunks _check-sdk-pin _check-spec-citations
# The profiler contributions are generated from the shared UI library ([PROFILER-SHARED-LIBRARY-VSCODE]).
node tools/vsix/sync-profiler-contributions.mjs --check
# The tutorial's types are generated from its typeDiagram model ([TUTORIAL-MODEL]).
node tools/vsix/tutorial-model.mjs --check
node --test tools/ci/security-gates.test.mjs tools/ci/changed-files.test.mjs tools/ci/vsix-coverage.test.mjs \
tools/ci/spec-citations.test.mjs tools/vsix/signals-bundle.test.mjs
npm run lint:eslint --prefix $(VSCODE_DIR)
Expand Down Expand Up @@ -1107,6 +1109,11 @@ _deploy-sidecars:
# The extension identifier, read from the manifest that defines it. Hardcoding it
# is how the previous uninstall target came to name an extension id that had not
# existed for months, silently uninstalling nothing. [DIST-VSIX-DEV-INSTALL]
# The VS Code profiles, joined by "|", that hold their own copy of SharpLsp: the
# install loop replaces it in each, not only in the default profile. Override
# with VSCODE_PROFILES='A|B', or VSCODE_PROFILES= for the default profile only.
# [DIST-VSIX-DEV-INSTALL]
VSCODE_PROFILES ?= $(shell node tools/vsix/vscode-profiles.mjs $(EXTENSION_ID))
EXTENSION_ID = $(shell node -e "const p=require('./$(VSCODE_DIR)/package.json');process.stdout.write(p.publisher+'.'+p.name)")

# The per-OS half of the install loop: where the VS Code CLI lives, and how stale
Expand Down Expand Up @@ -1154,13 +1161,17 @@ RESOLVE_CODE = \
# SharpLsp loaded in VS Code pretending to be the change under test.
# `_build-vsix` pulls in `_build-rust` and `_build-dotnet`, so the Rust host and
# both sidecar binaries are rebuilt here, not reused from the cleaned tree.
#
# The profiles holding SharpLsp are read BEFORE the uninstall, which removes it
# from them, and handed to both ends of the loop.
reinstall-vsix:
@echo "==> Uninstall, full clean, rebuild and reinstall for $(HOST_PLATFORM)..."
$(MAKE) uninstall-vsix
$(MAKE) _kill
$(MAKE) _clean-artifacts
$(MAKE) _build-vsix
$(MAKE) install-vsix
@profiles='$(VSCODE_PROFILES)'; \
$(MAKE) uninstall-vsix VSCODE_PROFILES="$$profiles" && \
$(MAKE) _kill && \
$(MAKE) _clean-artifacts && \
$(MAKE) _build-vsix && \
$(MAKE) install-vsix VSCODE_PROFILES="$$profiles"

install-vsix:
@test -f $(DEV_VSIX) || { \
Expand All @@ -1169,15 +1180,22 @@ install-vsix:
}
@echo "==> Installing $(DEV_VSIX)..."
@$(RESOLVE_CODE); \
"$$code_cli" --install-extension "$(DEV_VSIX)" --force
"$$code_cli" --install-extension "$(DEV_VSIX)" --force || exit 1; \
list='$(VSCODE_PROFILES)'; IFS='|'; for profile in $$list; do \
echo "==> ...and into VS Code profile '$$profile'"; \
"$$code_cli" --install-extension "$(DEV_VSIX)" --force --profile "$$profile" || exit 1; \
done
@echo "==> $(EXTENSION_ID) installed. Reload VS Code to pick it up."

# Uninstalling what is not installed is a success, not a failure: the reinstall
# loop runs this on a clean machine too.
uninstall-vsix:
@echo "==> Uninstalling $(EXTENSION_ID)..."
@$(RESOLVE_CODE); \
"$$code_cli" --uninstall-extension "$(EXTENSION_ID)" || true
"$$code_cli" --uninstall-extension "$(EXTENSION_ID)" || true; \
list='$(VSCODE_PROFILES)'; IFS='|'; for profile in $$list; do \
"$$code_cli" --uninstall-extension "$(EXTENSION_ID)" --profile "$$profile" || true; \
done

# ── Install (private) ─────────────────────────────────────────────

Expand Down
40 changes: 40 additions & 0 deletions docs/models/vscode-tutorial.td
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
typeDiagram

# The interactive tutorial's model ([TUTORIAL-MODEL]). The extension's
# src/tutorial/model.generated.ts is generated from this file by
# tools/vsix/tutorial-model.mjs; never edit that file by hand.

# A language the tutorial teaches. F# is taught first.
union TutorialLanguage {
FSharp
CSharp
}

# One thing the user does in the live editor.
union TutorialAction {
# Open a source file of the language in an editor.
OpenFile
# Hover a symbol, and SharpLsp answers with Quick Info.
Hover
# Trigger completion, and SharpLsp answers with items.
Completion
# Go to a symbol's definition, and SharpLsp answers with a location.
Definition
# Find all references, and SharpLsp answers with locations.
References
# Invoke code actions (Cmd+. / Ctrl+.), and SharpLsp answers with fixes and refactorings.
CodeAction
# Show the Solution Explorer.
SolutionExplorer
}

# A step of one language's walkthrough.
type TutorialStep {
language: TutorialLanguage
action: TutorialAction
}

# Every step the user has carried out, persisted across sessions.
type TutorialProgress {
done: List<TutorialStep>
}
25 changes: 23 additions & 2 deletions docs/specs/DESIGN-SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ Page-level grids and marketing card styles must not leak into `.prose`. Do not r

Screenshots and video of a SharpLsp feature show the real extension, never a mock-up.

- **Capture.** Product films are shot by `src/website/tools/profiler-film/` (its README says how). Every shot runs on `kit/set.mjs`'s `onSet`: an isolated VS Code (its own user-data and extensions directories, never the user's editor) running the packaged VSIX against a real .NET demo from `demos/`. A walkthrough is one file in `films/`, a list of scenes that `film.mjs` shoots as real-time clips and publishes; `kit/acts/` tells the shared stories (the CPU and memory loops, the language features, the Solution Explorer), so each film supplies only its words, its demo and its edits. The profiler films run on the C# Storefront demo and its F# twin, FsStorefront; the language films run on `Shop.slnx`, both in one solution, and show F# before C#. Every F# claim the docs make is shown on F# code. Every feature page's still, and every figure in the launch post, is shot the same way by `films/doc-stills.mjs`; no page shows a capture from any other source. The composited one-minute film is `films/profiler/`: `capture.mjs` records stills and screencasts, and `render.mjs` turns them into the site's assets.
- **Capture.** Product films are shot by `src/website/tools/video/` (its README says how). Every shot runs on `kit/set.mjs`'s `onSet`: an isolated VS Code (its own user-data and extensions directories, never the user's editor) running the packaged VSIX against a real .NET demo from `demos/`. Each film is a folder of its own, `films/<name>/`, holding only its words, timings and edits (`film.mjs`, and `narration.json` when narrated); all shared code is in `kit/`. A walkthrough's `film.mjs` is a list of scenes that the entry point `film.mjs` shoots as real-time clips and publishes through the one film camera ([WEB-DESIGN-MEDIA-CAMERA]); `kit/acts/` tells the shared stories (the CPU and memory loops, the language features, the Solution Explorer), so each film supplies only its words, its demo and its edits. The profiler films run on the C# Storefront demo and its F# twin, FsStorefront; the language films run on `Shop.slnx`, both in one solution, and show F# before C#. Every F# claim the docs make is shown on F# code. Every feature page's still, and every figure in the launch post, is shot the same way by `films/doc-stills/`; no page shows a capture from any other source. The composited one-minute film, `films/profiler/`, goes through the same entry point: its `film.mjs` names its own shoot (`capture.mjs`, stills and screencasts) and its own publish (`render.mjs`, which steps `film.html` frame by frame into the site's assets).
- **Lessons.** A lesson teaches one feature end to end: a narrated slide film (`kit/lesson/`) that names the shortcut, explains where the feature's results come from, shows each step on real footage, shows the code before and after, explains what happens underneath, and recaps. Its footage is cut from the feature's published walkthrough, cropped clear of the walkthrough's step caption, so it never shows a mocked editor, and a fast-forwarded stretch keeps its speed badge. Slides are typography and diagrams only; code shown on a slide is text, never a drawing of the editor. Each slide holds until its lines are spoken, and each item appears as the line naming it starts. New lessons start from `templates/lesson/`; `films/refactoring-lesson/` is the reference. The docs page the lesson teaches leads with it, follows its steps in prose with code to try, and keeps the walkthrough as the one-take recording.
- **Honesty.** Footage plays in real time unless the frame says otherwise: every fast-forwarded stretch carries an on-screen speed marker, and captions name time-lapses. Motion graphics (title cards, camera moves, highlights, the cursor) frame the footage but never alter what the editor shows.
- **Privacy.** No frame may show a user name, home directory or hostname. The demo runs from a neutral temporary path, and the editor runs on a copy-on-write clone of the .NET install under that path, with its login-shell environment off, so every path it prints is neutral. A terminal appears only where the feature runs in one (F# Interactive, a build task, a contributor's command line), and a shell terminal starts with no profile and a bare `$ ` prompt.
- **Formats.**
Expand All @@ -136,10 +137,30 @@ Screenshots and video of a SharpLsp feature show the real extension, never a moc
- Marketing pages may autoplay a film only when it is muted and looping; docs pages use controls and are never muted.
- Every autoplaying video pauses and shows its controls under `prefers-reduced-motion: reduce`.
- Every video ships a WebP poster, the films and the recordings alike.
- **Narration.** A walkthrough may be narrated (`kit/sound/`). Its reviewed script (`films/<film>.narration.json`) is spoken by Kokoro-82M through `kokoro-js` on the capturing machine, never by a speech service or the operating system's voices, whose licences forbid publishing. Each line starts just after its scene's cut, and a scene holds its last frame until its line ends. The music is an original score synthesised from code (`kit/sound/score.mjs`), so no film carries licensed or sampled music, and it ducks under the voice. A narrated film ships AAC in its MP4 and Opus in its WebM, plays unmuted with controls, and carries WebVTT captions in every site language (`<id>.<lang>.vtt`, the page's own language shown by default), listed under `captions` in the catalogue.
- **Narration.** A walkthrough may be narrated (`kit/sound/`). Its reviewed script (`films/<name>/narration.json`) is spoken by Kokoro-82M through `kokoro-js` on the capturing machine, never by a speech service or the operating system's voices, whose licences forbid publishing. Each line starts just after its scene's cut, and a scene holds its last frame until its line ends. The music is an original score synthesised from code (`kit/sound/score.mjs` for walkthroughs, the quicker `kit/sound/drive.mjs` for lessons), so no film carries licensed or sampled music, and it ducks under the voice. A narrated film ships AAC in its MP4 and Opus in its WebM, plays unmuted with controls, and carries WebVTT captions in every site language (`<id>.<lang>.vtt`, the page's own language shown by default), listed under `captions` in the catalogue.
- **One catalogue.** Every video on the site is declared once in `src/website/src/_data/videos.json`: encodings, size, length, upload date, the `page` that explains it, and its label and caption in each language. Every video also ships a 1200x630 JPEG social card, `<id>-card.jpg`, cut from its poster frame by `kit/encode.mjs`. Pages place a video with the `{% video "<id>" %}` shortcode (`src/website/video-figure.js`) and never repeat `<video>` markup. The shortcode renders in the page's language and emits a schema.org `VideoObject` whose name is the player's label, so search results describe exactly what a reader hears.
- **Placement.** A video goes wherever a page talks about what it shows: the home page (the profiler film autoplaying in the hero, the CPU recording in the profiler section), every docs page with a feature to show (its own narrated walkthrough, after the page's introduction; the profiler docs add both recordings, the F# language page the F# profiling walkthrough too), and every blog post that discusses the profiler or the editor experience, in every language the page is published in. The Zed page has no film: Zed is a native application, and filming it would mean recording the real desktop.

## [WEB-DESIGN-MEDIA-CAMERA] Film Camera

Every walkthrough is cut through one camera, `kit/zoom.mjs` in `src/website/tools/video/`, so the films move alike and no film keys a camera move by hand.

- **Track.** While a clip records, the workbench samples twenty times a second where the film's pointer and the editor's text cursor are, and every widget the editor has open beside them: a hover, the completion list and its details, signature help, a peek, the rename box, a code-action or context menu, a dialog, the Settings editor and the Problems panel. A widget is framed once it has stayed open a moment, and the palette only when a film dwells on it. Each step caption is logged with its time (`kit/track.mjs`). The shoot saves the track beside its clip.
- **Framing.** A clip opens on the whole workbench. The camera then pushes in on the pointer or the text cursor, whichever moved last, framed where it comes to rest rather than along the way it glides or jumps, together with every open widget and everything that happens in the next 1.5 seconds, as close as 2× and never past the workbench's edge. It holds while the action stays in frame, and moves when the action leaves it. After two quiet seconds with nothing open, it pulls back to the whole workbench when the quiet lasts at least 2.5 seconds longer.
- **Motion.** Every move eases in and out over 0.8 seconds, starting 0.35 seconds before the action it frames. Fast-forwarded footage is paced so its moves look the same on screen.
- **Sharpness.** Each frame of the cut is cropped from the 3200×2000 footage and scaled to the film's 1920-pixel width, so the deepest zoom loses no detail.
- **Captions.** The step caption and speed badge are laid over the cut after the camera (`kit/captions.mjs`), in the same place in every frame, so the camera never moves or crops them. The camera keeps what it frames clear of their band at the foot of the frame.
- **Poster.** A film's poster is a frame of the cut as it plays, camera and caption included.

## [WEB-DESIGN-MEDIA-CHAPTERS] Film Chapters

A film marks the runs of its cut that show one step of the VS Code tutorial ([TUTORIAL-STEPS]), so the tutorial panel ([TUTORIAL-PANEL]) plays a step's own run, not the whole film from a guessed second.

- **Marks.** A scene names its step with `chapter` (`scene(clip, n, text, act, { chapter })`): a step key such as `FSharp.Hover`, or the action alone (`SolutionExplorer`) when both languages share the shot. Consecutive scenes with one mark make one chapter; a mark on scenes apart fails the film. The scene's step caption titles the chapter.
- **Track.** Publishing writes `<id>.chapters.vtt` beside the video (`kit/chapters.mjs`, through `kit/vtt.mjs`, the one WebVTT writer captions use too): one cue per chapter, its identifier the mark, timed in the cut with every hold before it. A film with no marks publishes no track.
- **The tutorial's film.** `films/vscode-tutorial/` is shot for the panel: one shot per tutorial step, F# first, each doing exactly what its step asks, so every step has a chapter of one film, framed by the camera ([WEB-DESIGN-MEDIA-CAMERA]).
- **On the site.** A chaptered film is catalogued with `"chapters": true`. Its player carries the track as `<track kind="chapters">`, and `{% chapters "<id>" %}` lists the chapters: each one's start, linked to the film's watch page at that second (`?t=`), and its step's title as the VS Code walkthrough shows it in the page's language (`walkthrough.<Language>.<Action>.title` in the extension's `package.nls*.json`; a step both languages share takes the F# title). The docs Tutorial page (`/docs/tutorial/`, Set up group) leads with `vscode-tutorial` and lists its chapters.

## [WEB-DESIGN-MEDIA-DISCOVERY] Video Search and Sharing

Every catalogued video can be found in Google's video results and plays in social previews. The rules follow Google's [video SEO best practices](https://developers.google.com/search/docs/appearance/video), [VideoObject structured data](https://developers.google.com/search/docs/appearance/structured-data/video) and [video sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps).
Expand Down
Loading
Loading