Author Instructions โ
How to write, preview, and publish tutorials on the SAP Developers tutorial platform.
This document describes the current authoring workflow and flags planned improvements in ยง11 below. If you have edit access to a repo under the sap-tutorials GitHub organization, you are the audience.
1. The big picture โ
You write Markdown Platform fetches & rebuilds Readers see HTML
โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
sap-tutorials/<repo>/ โโโถ tutorials-ims CI โโโถ developers.sap.com
*.md + images (fetch โ Hugo โ publish) /tutorials/<slug>You never touch the rendering pipeline, the Hugo site, or the database. You write Markdown in a tutorial repo, open a pull request, and after it is merged the platform takes over.
2. Where tutorials live โ
Tutorials are stored in repos under the sap-tutorials GitHub organization. Each repo holds one tutorial set (typically grouped by product or topic). A single Markdown file in that repo represents one tutorial and becomes one URL on the platform.
Naming convention:
| File | Becomes |
|---|---|
sap-tutorials/abap-core-development/tutorials/abap-cloud-ui-from-interface.md | /tutorials/abap-cloud-ui-from-interface |
sap-tutorials/abap-core-development/tutorials/abap-cloud-ui-from-interface/001-find-interface.png | image referenced from the tutorial |
The filename (without .md) becomes the slug โ the public URL segment. Slugs must be unique across the entire sap-tutorials org.
Quiz / validation files โ
If your tutorial has a quiz, validation rules live in a parallel <repo>-Contribution repo (private) under the same org as a rules.vr file with the same slug. The platform fetches it automatically when the build runs.
You can also have the platform generate quiz questions for you at build time with an [AUTOAUTHOR_*] directive in rules.vr (see the AUTOAUTHOR live example):
[AUTOAUTHOR_3]/[AUTOAUTHOR_ALL]โ generate questions from the step's text (add:mcqor:textto restrict the type).[AUTOAUTHOR_VIDEO_3 url=https://youtu.be/โฆ]/[AUTOAUTHOR_VIDEO_ALL url=https://youtu.be/โฆ]โ generate questions from a YouTube video's transcript instead of the step text. Use the per-step form to quiz on the video referenced by one step, or the_ALLform for one quiz covering the whole tutorial's video. Add:mcq/:textthe same way.
A hand-authored [VALIDATE_N] question for a step always wins over any [AUTOAUTHOR_*] directive for that step โ the generated questions are opt-in extra coverage, never an override. If a video has no captions the generated quiz is simply skipped; your build still succeeds.
3. Anatomy of a tutorial โ
โ Live examples โ see the Tutorial Platform Features for Authors mission for working tutorials that demonstrate every piece of syntax described below: the CODECHECK demo, free-text grading, AUTOAUTHOR, and the feature cookbook (OS variants, branches, mermaid, codetabs, glossary, lightbox).
A tutorial Markdown file has three required parts: frontmatter, an introduction, and steps.
3.1 Frontmatter โ
YAML block at the very top of the file, fenced with ---:
---
parser: v2
auto_validation: true
primary_tag: programming-tool>abap-development
tags: [tutorial>beginner, programming-tool>abap-development, software-product>sap-business-technology-platform]
time: 15
author_name: Jane Doe
author_profile: https://github.com/janedoe
---| Field | Required | Notes |
|---|---|---|
parser | Yes | Always v2 for new tutorials. V1 ([ACCORDION-BEGIN] markers) is legacy. |
primary_tag | Yes | One tag from the platform taxonomy โ drives categorization. |
tags | Yes | Array. First tag should be tutorial>beginner, tutorial>intermediate, or tutorial>advanced. |
time | Yes | Estimated minutes to complete. Integer. |
auto_validation | No | Set true if the tutorial has quiz rules in the -Contribution repo. |
author_name, author_profile | Recommended | Shown on the tutorial page. |
video | No | Optional intro video shown as a styled player at the top of the steps. See ยง3.3 Adding a video. |
variables | No | Array of substituted values โ either per-reader (e.g. a workshop group number the reader fills in) or author-fixed (value:, optionally display: false to hide). The platform substitutes them live throughout the tutorial. See ยง3.7 Personalize with variables. |
A canonical taxonomy of allowed primary_tag and tags values is maintained in the platform's Tags entity. To add a new tag or category, see Center Admin ยง "Import a new tag".
3.2 Title and introduction โ
Immediately after the frontmatter:
# Generate your own custom UI Service based on a Business Object Interface
<!-- description -->Business Object Interfaces are provided by SAP in order to release business objects...
## You will learn
- How to generate a custom UI Service based on a C1-released Business Object Interface
- How to identify the authorization objects required to consume it
## Prerequisites
- SAP BTP, ABAP Environment
- A package located in the software component ZLOCAL
---# Title(single H1) โ the tutorial title.<!-- description -->...โ a comment marker followed by the short description shown in catalog pages and search results.## You will learnand## Prerequisitesโ H2 sections, conventional and recommended.- The standalone
---after Prerequisites separates the introduction from the steps.
NOTE
Both sections auto-hide when empty. If a tutorial has no ## Prerequisites content, the Prerequisites section and its tab are omitted entirely (no empty heading). Likewise, a tutorial with no steps renders no "Steps" heading or tab. This is how you retire a tutorial down to a redirect note: remove the Prerequisites bullets and all ### steps, and keep just an intro paragraph or blockquote (e.g. an "out of maintenance" note pointing to the SAP Discovery Center). The intro prose still renders; only the empty section chrome is dropped.
TIP
Only include a single contiguous set of bullet points in the Prerequisites section, and don't include any normal paragraphs. Otherwise the rendered version may be unpredictable and not what you intend.
3.2.1 Adding a video โ
Many tutorials include a companion video. There are two supported ways to add one; the frontmatter approach is preferred for a single intro video.
Preferred โ video: frontmatter. Add a video object to the frontmatter. The platform renders it as a styled, responsive 16:9 player at the top of the Steps section:
---
parser: v2
primary_tag: products>sap-hana-cloud
tags: [tutorial>beginner, products>sap-hana]
time: 20
author_name: Jane Doe
author_profile: https://github.com/janedoe
video:
url: https://www.youtube.com/watch?v=6WY70LyLS1c
title: Create a User Interface with CAP
---| Field | Required | Notes |
|---|---|---|
video.url | Yes | A YouTube watch/short/embed URL (or a bare 11-char YouTube ID), a Vimeo URL, or an openSAP microlearning / SAP video URL. |
video.title | No | Accessible title for the player. Defaults to Video tutorial. |
Supported hosts: YouTube (youtube.com, youtu.be), Vimeo (vimeo.com, player.vimeo.com), microlearning.opensap.com, and the SAP video service. An unrecognized host is ignored (no broken frame) โ the build logs a warning naming the slug.
Also supported โ raw HTML in the body. You can still embed a player inline anywhere in the tutorial (including a ## Video Version section before the first step) using a raw <iframe>:
## Video Version
<iframe width="560" height="315" src="https://www.youtube.com/embed/6WY70LyLS1c" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>The iframe host must be on the platform allowlist (same hosts as above) or it is stripped for security. <video> elements are also supported. See the platform reference iframe allowlist.
TIP
Don't do both for the same video. If you set video: frontmatter, remove any ## Video Version iframe so the player isn't rendered twice.
3.3 Steps (V2 parser) โ
Each step is a ### H3 heading followed by content:
### Find the released Business Object Interface
1. In the Project Explorer, select **Released Objects** โ `USE_IN_CLOUD_DEVELOPMENT`.
2. Right-click on the interface `I_BankTP` and select **Generate ABAP Repository Objects**.

### Configure the generated artifacts
Continue with the next step...Rules of thumb:
- One H3 per step. Avoid H4/H5 inside a step โ they don't render as nested navigation.
- Step titles become the table of contents on the right of the rendered page.
- Step content is plain Markdown: lists, code fences, images, links, bold/italic.
- Inline HTML is escaped for safety. Stick to Markdown.
3.3.1 Step time and optional steps โ NEW โ
Add a trailing {โฆ} attribute block to a step heading to declare a per-step time estimate and/or mark the step optional:
### Deploy the app {time=5m}
### Enable the trial entitlement {time=2m optional}time=<duration>โ5m,90s,1h, or1h30m(max 8h). Renders aโฑ N minchip on the step and contributes to the tutorial's total time (the page's frontmattertime:still wins when both are set; the parser warns in the build log when the two disagree by more than 25%). Readers also see a live "โ N min left" estimate above the steps once at least half the steps declare a time.optionalโ renders an "Optional" badge and a "Skip" button next to "Done". Completing a tutorial never requires an optional step โ mark a step optional when it only applies to some readers (e.g. "if you don't already have a trial account"). At least one step must stay required; a tutorial where every step is optional fails the build.- The attribute block is stripped before the title is shown or slugged, so adding
{time=5m}to an existing heading is a safe, non-breaking edit โ it does not change the step's anchor, anyosOverrideskey, or existing readers' progress. - Unrecognised content in the braces (e.g.
{status: ok}that isn't step-pacing syntax) is left in the title untouched and reported as a lint warning โ it never silently disappears.
3.3.2 Interactive checklists โ NEW โ
A standard GFM task list inside a step renders as live, checkable checkboxes instead of plain bullets:
- [ ] Create a subaccount
- [ ] Enable the entitlement
- [x] Already have a BTP account (pre-checked โ shown checked, doesn't count toward the counter)Each unchecked item becomes a clickable checkbox; a small "x/y" counter above the list tracks how many the reader has ticked, and the state persists in the reader's browser across reloads. [x] items are rendered checked but excluded from that counter โ use them for "already done" context items, not for things you want the reader to actually tick off. Checking every toggleable item in a step visually highlights that step's Done button, but never marks the step done automatically โ the reader still clicks Done.
3.4 Images โ
Place images alongside the Markdown file in a folder named after the slug:
abap-cloud-ui-from-interface.md
abap-cloud-ui-from-interface/
001-find-interface.png
002-start-generator.pngReference them with relative paths:
The platform automatically resolves these to raw.githubusercontent.com URLs at build time. Do not hardcode https://github.com/... URLs.
3.5 Option blocks โ
When a step has variants, wrap each variant in an OPTION block. There are two flavors โ generic (per-step tabs) and OS-conditional (driven by a global picker at the top of the tutorial).
3.5.1 Generic option blocks (Java vs Node, JSON vs XML, Cloud vs On-premise) โ
[OPTION BEGIN [JSON]]
...content for the JSON path...
[OPTION END]
[OPTION BEGIN [XML]]
...content for the XML path...
[OPTION END]The platform renders these as a tab strip inside the step. Each step's tabs are independent.
3.5.2 OS-conditional content (Windows vs macOS vs Linux vs BAS) โ NEW โ
When the variants are about the operating system, the platform automatically detects this and wires every OS block on the page to a single global picker at the top of the tutorial. The reader picks their OS once; their choice persists across tutorials.
Use any of these labels โ they're all recognized:
| Canonical OS | Recognized labels |
|---|---|
| Windows | Windows, Win, Win32, Win64 |
| macOS | macOS, MacOS, Mac OS, Mac, OS X, Darwin |
| Linux | Linux, Ubuntu, Debian, Fedora, Unix |
| BAS | BAS, Business Application Studio, SAP BAS |
Combined labels are fine โ Mac and Linux matches both, Mac & Linux likewise.
[OPTION BEGIN [Windows]]
Open PowerShell and run `cd $HOME\projects`
[OPTION END]
[OPTION BEGIN [Mac and Linux]]
Open a terminal and run `cd ~/projects`
[OPTION END]The reader sees only the variant matching their OS. Their choice persists across tutorials.
Defaults & detection. First-time visitors get auto-detected (Windows / macOS / Linux from the browser; BAS detected when the tutorial is opened from inside Business Application Studio). After they pick an OS, that choice is remembered.
Missing variants. When a step doesn't cover the reader's chosen OS, the platform shows a placeholder message โ "No <OS> instructions for this step." โ instead of substituting another OS's content. No automatic closest-match substitution is made. To avoid gaps, include a panel for every OS you expect readers to use, or omit the group entirely if the step is OS-agnostic.
Existing OS-tabbed tutorials get the picker for free. No author migration required โ the heuristic detects OS-flavored OPTION blocks automatically.
Author override. If the auto-detection mis-classifies your tabs (e.g. you have a tab named Linux that's actually about a Linux container product, not the OS), add to your frontmatter:
osOverrides:
step-3-deploy-the-app: regular # force this step's group to NOT be OS-conditional
step-5-install-cli: os # force this step's group to BE OS-conditionalThe key is the slugified step heading.
3.5.3 AI-assisted OS variants (VS Code) โ NEW โ
The Tutorials VS Code extension can generate the missing OS variants for you. Write your step for one OS, then ask the extension to "generate OS variants" โ it returns translated Windows / macOS / Linux / BAS blocks you can review and accept inline. See the [VS Code extension docs](TODO: link added when plugin ships) for the workflow.
3.5.4 Synced option tabs โ NEW โ
When the same choice (language runtime, account tier, IDE) repeats across multiple steps, add sync=<key> to each group's first OPTION block. Readers pick once from the page-level picker bar and every group updates automatically โ their preference is also remembered across tutorials.
Built-in preset keys (automatically alias common label variants):
| Key | Canonical values | Example aliases |
|---|---|---|
runtime | Node.js, Java | Node, CAP Node.js, CAP Java, Spring โ Java |
account | Trial, Enterprise | Pay-As-You-Go, PAYG โ Enterprise |
ide | BAS, VS Code | Business Application Studio โ BAS |
Usage:
[OPTION BEGIN [Node.js] sync=runtime]
Node.js content here.
[OPTION END]
[OPTION BEGIN [Java] sync=runtime]
Java content here.
[OPTION END]The same sync=runtime key must appear on the first block of every group in a step. Conflicting keys within one group are a build error.
Auto-classification (syncTabs: auto):
Add syncTabs: auto to your frontmatter to let the build classify groups automatically โ useful when you have many steps with the same runtime choice and don't want to annotate each block individually:
---
title: My Tutorial
syncTabs: auto
---Tip:
syncTabs: autoonly classifies groups whose labels ALL match one preset's aliases. A group with mixed or unrecognised labels is left as a plain option group. Runnpm run fetch-tutorials -- --dry-runor inspect thesyncGroupsfrontmatter in the generated Hugo content to see which groups were classified.
Deep links:
Add ?runtime=Java (or ?runtime=java โ case-insensitive) to a tutorial URL to pre-select a choice. The selection is persisted for the reader's subsequent visits.
Custom sync keys:
Declare a non-preset key in syncGroups frontmatter:
syncGroups:
db:
label: Database
values: [HANA, PostgreSQL]Then use sync=db on OPTION groups with labels from that list.
Alert callouts โ
A plain blockquote (>) renders as the default blue Information callout. To signal a different kind of emphasis, start the blockquote with a GitHub-style alert designator on its own first line:
> [!TIP]
> Prefer the frontmatter `video:` field over an inline iframe.Alert types reference โ
| Type | Icon | Default title | Collapsible | Notes |
|---|---|---|---|---|
> [!NOTE] | info circle | โ | no | General information |
> [!TIP] | lightbulb | โ | no | Helpful aside or shortcut |
> [!IMPORTANT] | hint | โ | no | Key information the reader must not miss |
> [!WARNING] | alert | โ | no | Risk of data loss or unexpected behavior |
> [!CAUTION] | error | โ | no | Severe or irreversible risk |
> [!DETAILS] Title | detail-view | โ | yes (closed) | Collapsible section; title is required |
> [!DETAILS+] Title | detail-view | โ | yes (open) | Same as DETAILS but open by default; title required |
> [!TROUBLESHOOT] Title | wrench | โ | yes (closed) | Contributes to the auto-generated troubleshooting index at the bottom of the tutorial; title is optional |
> [!RESULT] | checkmark | "You should see" | no | Expected outcome after completing a step |
> [!HINT] | hint | โ | yes (closed) | Multiple HINT callouts in the same step get progressive reveal โ the second hint unlocks only after the first is opened |
> [!SOLUTION] | complete | โ | yes (closed) | Full solution; place at the end of an exercise |
Pick the designator by intent, not loudness: use [!TIP] for helpful asides, [!WARNING]/[!CAUTION] only for genuine risk. An ordinary "additional note" can stay a plain > (blue) or use [!NOTE].
Optional titles โ
Any alert type accepts an optional title on the same line as the designator:
> [!NOTE] Important setup step
> This note has a custom heading above its body.DETAILS and DETAILS+ require a title. All other types treat the title as optional.
Collapsible callouts โ open by default (+) โ
Append + to make a collapsible callout start in the open state:
> [!DETAILS+] Show me the full config
> ```yaml
> key: value
> ```Lint warning: A collapsed callout (
DETAILS,HINT,SOLUTION,TROUBLESHOOT) that contains the step's only command block will trigger a build warning. Move the command block outside the callout, or mark the callout open with+.
RESULT callouts โ
RESULT provides a default title of "You should see" and a checkmark icon. Use it immediately after the action the reader should verify:
> [!RESULT]
> The terminal prints `Build succeeded` and exits with code 0.Note: Automatic verification links (assert-link) are deferred to a follow-up.
HINT and SOLUTION callouts โ
Use HINT for progressive nudges inside an exercise. Multiple hints in one step unlock one-at-a-time so the reader must open the first before the second appears:
> [!HINT]
> Look at the `requires` section of `package.json`.
> [!HINT]
> The value you need is `@sap/cds`.
> [!SOLUTION]
> Add `"@sap/cds": "^8"` to your `dependencies`.Directive equivalents โ
Some callout types have an alternative :::type[Title] directive syntax โ useful when the body contains complex markdown that would be awkward inside a > blockquote:
| Directive | Equivalent blockquote form |
|---|---|
:::details[Title] | > [!DETAILS] Title |
:::troubleshoot[Title] | > [!TROUBLESHOOT] Title |
:::result | > [!RESULT] |
:::hint[Optional title] | > [!HINT] Optional title |
:::solution[Optional title] | > [!SOLUTION] Optional title |
Example:
:::details[Full configuration file]
```yaml
key: value
nested:
key: value:::
#### Inline spoilers
Use `:spoiler[text]` to blur text until the reader clicks it โ useful for quiz answers or
reveal-on-demand content:
```markdown
The answer is :spoiler[42].In plain-text contexts (MCP, email) the spoiler is rendered as ||42||.
Plain-text fallback forms โ
When a tutorial is consumed in a plain-text context (MCP tool, email digest) the platform renders simplified equivalents:
| Feature | Plain-text form |
|---|---|
| DETAILS / TROUBLESHOOT / HINT / SOLUTION | **Details: Title** followed by body |
Inline spoiler :spoiler[text] | ||text|| |
3.6 Code blocks โ
Use fenced code blocks with a language tag. Supported languages include abap, js, ts, java, cds, sql, bash, yaml, json, xml, html, css. CDS code uses dedicated highlighting (see npm run build:highlight).
\`\`\`cds
entity Books : managed {
key ID : Integer;
title : localized String(111);
}
\`\`\`The platform extends standard fenced code blocks with five additional features described below.
3.6.1 Console blocks โ
Tag a fence console, shell-session, or terminal-session โ or add {prompt} to any language fence โ to enable prompt/output separation. Lines starting with a recognised prompt ($, >, PS>, #) are commands; all other lines are visually dimmed output. The copy button copies only the command lines, with prompts stripped.
Continuation: a command line ending with \ folds with the next continuation line into one copyable command.
\`\`\`console
$ cf login -a https://api.cf.eu10.hana.ondemand.com
API endpoint: https://api.cf.eu10.hana.ondemand.com
$ cf target -s dev
org: myorg space: dev
\`\`\`Copied text: cf login -a https://api.cf.eu10.hana.ondemand.com\ncf target -s dev
Existing bash fences without {prompt} are unchanged.
3.6.2 Diff and focus markers โ
Add trailing comment markers to individual lines to annotate diffs without changing the code itself. The markers are stripped from the rendered display.
| Marker | Effect |
|---|---|
// [!code ++] | Line is added (green highlight) |
// [!code --] | Line is removed (red, struck-through, excluded from copy) |
// [!code focus] | Line is focused (non-focused lines dimmed until hover) |
// [!code highlight] | Line is highlighted (yellow accent) |
Comment syntax adapts to the language: // for JS/TS/Go/Java, # for Python/YAML/Bash, <!-- for HTML/XML.
\`\`\`js
function greet(name) { // [!code --]
function greet(name = 'World') { // [!code ++]
return \`Hello, \${name}!\`
}
\`\`\`Copy default: the final code (removed lines excluded, markers stripped). Use {copy=raw} to copy source with markers intact, useful for teaching diffs.
3.6.3 Fill-in placeholders โ
When a tutorial uses values that are different for every reader but not a personalised variable (e.g. <YOUR_SUBACCOUNT_ID>), opt in to placeholder detection. The platform converts each token into a fillable chip backed by the variables system โ filling it once updates every occurrence and the value persists across page refreshes.
Auto-detect (recommended):
placeholders: autoThe parser scans for tokens matching <YOUR_โฆ>, <MY_โฆ>, or <THREE_OR_MORE_UPPERCASE_CHARS>. It skips TypeScript/Java generics such as Array<T> or List<String>.
Explicit list (when auto-detection finds false positives):
placeholders:
- token: <YOUR_SUBACCOUNT_ID>
label: Your BTP subaccount ID
- token: <MY_CF_SPACE>
label: Your Cloud Foundry space nameUnfilled placeholders display and copy as the original token text (e.g. <YOUR_SUBACCOUNT_ID>).
3.6.4 File tree fences โ
Tag a fence tree to render a directory structure as an accessible, collapsible tree:
\`\`\`tree
my-project/
package.json
src/
index.ts
+ srv/
+ cat-service.ts # new: CAP service handler
~ db/schema.cds # modified: added new entity
- old-handler.js # deleted
\`\`\`Syntax:
- Indentation (2-space) or standard box-drawing characters (
โโโ,โโโ,โ) indicate hierarchy. - Trailing
/marks a folder. - Leading
+/~/-prefixes mark new / modified / deleted items (rendered in green / blue / red). # text(two or more spaces then#) adds an inline annotation.
The copy button returns the plain ASCII tree source.
3.6.5 Walkthroughs (:::walkthrough) โ
A walkthrough pairs a code fence with an ordered list of notes. Each note starts with a line range in braces. Clicking or keyboard-navigating a note highlights the corresponding lines in the code.
:::walkthrough
\`\`\`js
import cds from '@sap/cds'
const app = cds.connect.to('catalog')
const { Books } = app.entities
app.on('READ', Books, req => {
return app.read(Books).where({ ID: req.data.ID })
})
\`\`\`
1. {1} Import the CAP SDK
2. {3} Connect to the catalog service
3. {4} Destructure the `Books` entity
4. {6-8} Register a READ handler and return the result
:::Rules:
- The body must contain exactly one fenced code block followed by a numbered list.
- Each list item must start with
{N}or{N-M}where N and M are 1-based line numbers within the fence. - Line references out of bounds fail the build with
directive/invalid-attr.
For plain-text consumers (MCP, AI tools), the walkthrough renders as the raw code fence followed by notes prefixed (lines NโM) .
3.7 Personalize with variables โ
When a tutorial contains a value that is different for every reader โ most often a workshop group or seat number โ declare it as a variable. The reader types it once into a field at the top of the page and the platform substitutes it live into every marked occurrence: prose, inline code, and copy-pasteable fenced code blocks alike. No more "replace XXX everywhere" instructions.
Declare variables in frontmatter under a variables: key โ one entry per value:
variables:
- name: groupNumber # identifier + body-token key; must match [A-Za-z][A-Za-z0-9_]*, unique
label: Your workshop group number # field label the reader sees
type: string # string | number
maxLength: 3 # optional: caps input length + drives validation
required: true # optional: marks the field required
placeholder: XXX # optional: shown until the reader types a valueReference a variable anywhere in the body with a {{var:<name>}} token โ the same syntax works in all three contexts:
In prose: your group number is {{var:groupNumber}}.
In inline code: your space is `dev-{{var:groupNumber}}`.```bash
cf create-space dev-{{var:groupNumber}}
```Optionally pre-fill the value from the reader's identity with a default regex (capture group 1 becomes the default):
default:
from: email # one of: email | id | givenName | familyName
pattern: 'da164-(\d+)@' # capture group 1 โ default valueAuthor-fixed values and hidden variables โ
Sometimes the value is the same for every reader but repeated throughout the tutorial โ a product name, a release version, a service URL. Set it once with value: and reference {{var:<name>}} everywhere; change it in one place and it updates across the whole tutorial. Add display: false so the reader never sees an input for it:
variables:
- name: productVersion
type: string
value: "MyApp 2.0" # author-fixed literal โ substituted everywhere
display: false # hidden: no input shown to the readerdisplayis optional and defaults totrue(shown). Onlydisplay: falsehides the input. A hidden variable needs nolabel.value:overrides adefault:regex. Precedence for the substituted value is:valueโdefault(regex) โ what the reader types โ empty.- A displayed variable (
display: trueor omitted) with avalue:is pre-filled with that value but the reader can still edit it.
Key behaviors:
- Build-enforced. A malformed
variables:block, an invalidtype, adefault.fromoff the allowlist{email, id, givenName, familyName}, an uncompilabledefault.pattern, or a{{var:<name>}}token referencing an undeclared name is a hard build failure โ it can never reach readers. (A declared-but-unused variable is a soft warning.) - Client-side only. Substitution happens in the reader's browser; nothing is rendered or stored server-side, so the feature is privacy-clean.
- Persists + carries over. A typed value survives a browser restart and carries across the tutorials in a mission by variable
nameโ use the same name across a mission for the same value. placeholderonly sets the greyed-out hint and the pre-substitution text; it is not a scan-and-replace target. Only{{var:<name>}}tokens are substituted โ a bareXXXin your text staysXXX.
Full worked walkthrough (self-demonstrating): Use Variables to Personalize Your Tutorials (source: sap-tutorials/meta-tutorials).
3.8 Directives โ NEW โ
Directives are a generic structured-content syntax for tutorials. They allow you to mark up passages that will be handled by specialized components, added to the platform in follow-up features. The directive system ships with no concrete directives yet โ it is the foundation only. Examples here use hypothetical directive names to show the syntax; they will not render as live components until those features land.
Directives have three forms:
Inline directives (mid-sentence):
This feature is available in `:menu[Production]{edition="prod"}` only.A directive spans from the opening : through the closing }. It has a name (kebab-case), an optional label in brackets, and optional attributes in braces.
Leaf directives (standalone, block-level):
Some text above.
::code-sample[Download the SDK]{url="https://example.com/sdk.zip" filename="sdk.zip"}
More text below.Leaf directives are alone on a line, prefixed with ::, and render as a self-contained block.
Container directives (wrapping other content):
:::details[Click to expand]
This content is hidden until the reader clicks the heading.
More paragraphs and **markdown** all work here.
:::Container directives open with ::: and close with :::. Content between them is Markdown, rendered normally. Nesting uses extra colons โ an outer container uses more colons than its inner content:
::::compare
Outer container (four colons).
:::figure[Screenshot A]{align="left"}
Inner figure (three colons).
:::
:::figure[Screenshot B]{align="right"}
Another inner figure.
:::
::::Attribute syntax:
- Quoted values:
key="value"orkey='value'โ use double or single quotes around values with spaces. - Unquoted values:
key=value(no spaces in the value itself). - Bare flags:
openโ treated asopen=true. - CSS classes:
.classnameโ shorthand forclass="classname". The directive's implementation controls which classes are allowed (you will get a build warning if you use a class not in its allowlist). - Element IDs:
#idโ shorthand forid="id". - Duplicate keys: an error; each key must appear once.
Examples:
:::panel[Expandable section]{open .highlight #my-panel}
This is a container with `open=true`, class `highlight`, and id `my-panel`.
:::
:code[JavaScript]{lang="js" theme='dark'}
:::details[Learn more]{class="advanced"}
For advanced users only.
:::Escaping and safety:
- Attribute values are always HTML-escaped by the framework โ you never need to escape manually, and HTML injection through attributes is not possible.
- Directives inside code fences, inline code, indented code blocks, HTML blocks, or YAML frontmatter are never interpreted โ they render literally as text. This keeps your Markdown safe when it happens to contain colons and braces that look like a directive but aren't.
Unknown directive names:
If you use a directive name that doesn't exist (e.g. :menu[...] before the menu feature ships), the text is left as-is (rendered literally) and you'll see a lint warning directive-unknown during the build. This warning never stops a build โ it's informational only. The intent is to make it safe to write Markdown that coincidentally looks like :something[...] without risking a hard failure.
When a concrete directive is added to the platform, the warning will stop and the directive will render as a styled component.
4. The author workflow today โ
1. Fork or branch in sap-tutorials/<repo>
2. Add or edit a .md file under tutorials/
3. Commit any new images in the slug-named folder
4. Open a PR against main
5. Get โฅ 1 approving review, then merge (direct pushes to main are blocked โ see below)
6. Merge to main โ repo dispatch event โ tutorials-ims CI
7. CI re-fetches your tutorial, rebuilds Hugo, publishes to HANA
8. Live at developers.sap.com/tutorials/<your-slug> within a few minutesIMPORTANT
Since September 2026, an org-wide OSPO ruleset blocks direct pushes to main. Every change โ even a typo fix โ must go through a reviewed pull request. Full rules and step-by-step branch/fork/PR instructions: branch-protection-and-pull-requests.md.
The notification step (6) is wired by adding a small workflow file to your tutorial repo โ see tutorial-repo-dispatch.yml. Org admins typically handle this once per repo.
5. Local preview โ
To see your tutorial render exactly the way it will on production:
git clone https://github.com/sap-tutorials/tutorials-ims.git
cd tutorials-ims
npm install
npm run fetch-tutorials # Pulls every tutorial from the org (cached after first run)
npm run dev # Hugo dev server at http://localhost:1313Open http://localhost:1313/tutorials/<your-slug> to see your page.
To force a re-fetch of just-merged content, delete .tutorial-cache/<your-slug>.sha and re-run npm run fetch-tutorials. To force a full re-fetch, delete the entire .tutorial-cache/ directory.
A GITHUB_TOKEN environment variable with repo:read scope is recommended; without it you may hit GitHub rate limits.
5.1 Previewing on the QA channel โ
The QA channel is a deployed author-preview environment separate from production. It renders content sourced exclusively from *-Contribution repos at /tutorials-qa/*, gated by the Tutorial.Author BTP scope.
Key points:
- What it shows โ only content from your
<repo>-Contributionrepo (the private companion that holds quiz rules and draft tutorial content). It is not a copy of the production channel. - Access requirement โ you must hold the
Tutorial.Authorrole collection in the BTP subaccount. To request it, post in the Tutorial Authors channel in Microsoft Teams or email thomas.jung@sap.com โ see Requesting QA channel access for what to include. - Setup โ the one-time infrastructure setup (CI secrets, dispatch-token distribution, HDI binding, role-collection creation) is documented in ../developers/operations/qa-channel-bootstrap.md. You do not need to repeat this; it is done once per environment.
- Local dev server โ your local
npm run devdoes not use any QA flag. The QA flag (hugo.qa.toml) only applies to the deployed QA build. Running locally is always sufficient for layout and step rendering checks. - Triggering a QA rebuild โ push a change to the
*-Contributionrepo. The same repo-dispatch mechanism used for production fires a QA-specific CI workflow that fetches from-Contributionrepos only, builds with the QA Hugo config, and publishes to the QA HANA instance. - URL pattern โ
https://<approuter>/tutorials-qa/<your-slug>(requiresTutorial.Authorscope; access is denied to unauthenticated users).
6. Pre-submit checklist โ
Run through this before opening your PR:
- [ ] Frontmatter is valid YAML (no tab characters, no missing quotes around tags with
:). - [ ]
parser: v2is set. - [ ]
primary_tag,tags,time, and the H1 title are present. - [ ] Tutorial has a
<!-- description -->line right after the H1. - [ ] Every step is an
### H3. No H1/H2 inside the step body. - [ ] All images referenced in the Markdown exist in the slug-named folder.
- [ ] Image paths are relative, not absolute GitHub URLs.
- [ ] Code fences have a language tag.
- [ ] Inline HTML is avoided (or limited to allowed tags only).
- [ ] If you used
OPTIONblocks, eachBEGINhas a matchingEND. - [ ] If you declared
variables:, every{{var:<name>}}token references a declaredname, and typing a value updates prose, inline code, and fenced code blocks (copy-paste one command to confirm your value is baked in). - [ ] Tutorial renders cleanly in
npm run devwith no warnings.
7. What happens after merge โ
| Step | Where | Duration |
|---|---|---|
| GitHub Action sends repo dispatch | Your tutorial repo | seconds |
tutorials-ims CI checks out, fetches all tutorials (cached) | GitHub Actions runner | ~3 s cached / ~2 min cold |
| Hugo rebuilds the static site | Runner | 5โ10 s |
| Delta publish: only changed slugs upload to HANA | CAP backend | 2โ4 s |
| LRU cache invalidates; next request serves new content | Production | immediate |
Total: typically under a minute for incremental edits, a few minutes for a full rebuild. See build.md for details.
If the build fails, the dispatch run will be red in the tutorials-ims GitHub Actions tab. Common causes:
- Bad YAML in frontmatter
- Missing image referenced from Markdown
- Unbalanced
OPTIONblocks or HTML tags - Slug collision with an existing tutorial
8. Updating and rolling back โ
Quick edits โ
Open a reviewed PR against main of the source repo (direct pushes to main are blocked โ see branch-protection-and-pull-requests.md). Once merged, the pipeline picks it up automatically โ no manual step.
Rollback โ
Production content is versioned. If a published change is broken, ask a Center Admin to roll back โ see center-admin.md ยง Content rollback. Rollback reverts the manifest pointer; you still need to follow up with a corrective PR.
9. (Reserved) โ
This section number is reserved for future use. See ยง11 for known gaps.
10. Where to ask for help โ
| Question type | Where |
|---|---|
| "Is my Markdown structured correctly?" | Open a draft PR; a repo group owner can review a draft PR and flag issues |
| "Why didn't my change appear on production?" | Check the tutorials-ims GitHub Actions runs; look for the failed dispatch |
| "How do I add a new tag / category?" | See Center Admin ยง "Import a new tag" โ taxonomy is centrally managed |
| "I need a preview before merging" | Run locally per ยง5, or request QA channel access (ยง5.1) โ see ../developers/operations/qa-channel-bootstrap.md (Tutorial.Author scope required) |
| Anything else | Platform team channel (internal) |
11. Known gaps and near-term improvements โ
These items are listed here so authors know what to expect โ and what not to expect โ from the current workflow.
| Gap | Current state |
|---|---|
| Editorial review gate | PR review now required by the OSPO ruleset (โฅ 1 approval before merge) โ see branch-protection-and-pull-requests.md; a formal Author QA lane is still separate |
| Approval / sign-off workflow | None โ merge equals publish; no reviewer roles |
| PR preview deploys | None โ preview requires local clone (ยง5) or QA channel (ยง5.1) |
| VS Code authoring extension | None โ live preview, frontmatter validation, link checking are planned |
| Reporting / analytics for authors | Available to platform admins only; author/management views not yet published |
| Tag taxonomy reference | Not yet author-facing; copy tags from a similar existing tutorial for now |
| Tag bulk import | Manual via Center Admin; CSV / API import planned |
| Per-tutorial rollback | Whole content-set rollback only; per-slug rollback not yet available |
| Precommit validation | Build errors land in .tutorial-cache/errors.json; author-facing npm run validate-tutorials partially in place but not published |
If any of these items become a blocker for your work, open an issue on the project's GitHub repository.
Reference: related docs โ
- build.md โ the full technical pipeline (fetch โ parse โ Hugo โ HANA)
- hugo-migration.md โ why Hugo, layout conventions
- tutorial-repo-dispatch.yml โ the GitHub Action your repo needs in order to trigger rebuilds
- repo-group-owners.md โ for repo owners reviewing your PRs
- center-admin.md โ for taxonomy, tag onboarding, and rollback
- ../developers/operations/qa-channel-bootstrap.md โ author-preview channel
- Use Variables to Personalize Your Tutorials โ self-demonstrating walkthrough of the
variables:feature (ยง3.7)