Pages
You start by creating pages as Markdown files (.md) in the source folder (./src is the default, but you can change this). The markdown pages should contain the following front matter.
---title: <title>---
Your article content starts here.- title:
string- The title of the page.
Optional front matter
Section titled “Optional front matter”Optional Front Matter properties are:
- slug:
string- If a slug is not defined, the folder the file is in and its title are used:guides/setup.mdtitled Set up becomesguides/set-up.aspx. You can add the slug with or without the.aspxfile extension;doctoradds it when it is missing. - draft:
boolean- defines if you want to publish the article during the publishing phase. Default: if not defined, the page will always be published. - description:
string- the page description to add. Be aware: description is limited to 255 characters. - comments:
boolean- with this setting you can enable/disable page commenting. By default comments are enabled, unless you disabled them for the whole site with thedisableCommentsoption. This page level setting always wins over the global one. - layout:
Article|Home- defines which page layout you want to use. Default layout type isArticle. - template:
string- the name of the page template to use for this page. Check: Page templates. - header:
HeaderOptions- defines how you want to render the header on the page.- type: Use one of the following values:
None|Default|Custom. Default:Default. - image: Path to the image file you want to use in your page header.
- altText: The image description.
- translateX: X focal point of the header image.
- translateY: Y focal point of the header image.
- layout: Layout to use in the header. Allowed values
FullWidthImage|NoImage|ColorBlock|CutInShape. Default:FullWidthImage. - textAlignment: How to align text in the header. Allowed values
Center|Left. Default:Left. - showTopicHeader: Specify if you want to show the topic header above the title. Default:
false. - topicHeader: Topic header text to show.
- showPublishDate: Show the publish date in the header. Default:
false. - authors:
string[]- The UPNs (for examplejohn@contoso.com) of the authors to show in the page header.
- type: Use one of the following values:
- menu:
Menu- Defines where the page gets added to the navigation structure. Check: menu section. - author:
number | string- Sets the page author (SharePoint’sAuthorcolumn). Use the site user ID — the id the user has in this site’s own user list — or their UPN (for examplejohn@contoso.com). Check: Author section. - metadata:
Metadata- With this object you can set extra metadata for your page. Check: Metadata section. - partials:
boolean | { header?: boolean, footer?: boolean }- Allows you to skip the partials which are added to every page with thepartials.headerandpartials.footeroptions. Usefalseto skip them all, or disable them one by one. Default: all configured partials are added. - localization:
{ [locale name]: relative path }[]- Defines the localization pages linked to the current page. Find out more at how to setup and use localization. - type:
string- Specifies the type of page. Currently it supports onlytranslationand should only be configured on localization pages. Find out more at how to setup and use localization.
When you want to create page to page links, you can provide the relative path from the current markdown file to the other markdown file (with or without the .md extension).
The menu property allows you to create a navigation structure for you static content. The Menu object has the following properties:
- menu
QuickLaunchORTopNavigationBar- Default isQuickLaunch- id:
string(required) - Navigation id. This can be used to create a hierarchy in your navigation. - name:
string(optional) - When this property is defined, it will be used for the navigation item title, otherwise the page title will be used. - weight:
number(optional) - The order. Items with a weight come first, lowest first; items without one follow, alphabetically.0counts as no weight. - parent:
string(optional) - Defines the hierarchy of your page in the menu. If not provided, the item is added to the root of the navigation. When defined, it is theidof the parent: another page’s menuid, or a static item indoctor.json. You can also add multi-level navigation like:<parent-id>/<sub-parent-id>. It is lowercased and its spaces removed before it is looked up.
- id:
Example 1
Section titled “Example 1”The following page will be added to the root of the QuickLaunch after the already defined links.
---title: Documentationslug: documentation.aspxdraft: false
menu: QuickLaunch: id: documentation weight: 1---
Write here the Doctor page content.Example 2
Section titled “Example 2”The following page adds a subpage underneath the documentation link in the navigation.
---title: Toolsslug: documentation/tools.aspxdraft: false
menu: QuickLaunch: id: tools weight: 1 parent: documentation---
Write here the tools page content.Example 3
Section titled “Example 3”Defines a new page under the tools section:
---title: Doctorslug: documentation/doctor.aspxdraft: false
menu: QuickLaunch: id: doctor weight: 1 parent: documentation/tools---
Write here the Doctor page content.How the page is built
Section titled “How the page is built”Doctor owns one section of the page: the markdown file is the page, so that section is rewritten to
exactly what the file says on every publish.
- The page banner keeps the full-width section at the top of the page, on its own. A full-width section holds a single web part, so nothing else is ever put there.
- The content goes in the first empty one-column section. If the page has none — a page which is only a banner, or whose sections all hold web parts already — one is created below them. A section which already has a web part in it is never taken over.
- Once a page has content, it stays in the section it is in. Re-publishing never moves it, and if you
drag it to another section in SharePoint it is published there from then on:
Doctorfinds its own web parts by the instance ids it recorded, so the section is wherever they are now. - Every other section is left untouched: a vertical section, and anything the
templatefront matter brings along.Doctorhas no way to describe a vertical section’s contents in markdown, so it never writes to one.
The banner follows the header front matter the same way. Changing a setting
applies it and resets the ones you left out, and removing the header block altogether puts the
banner back to the default — a page never keeps a header its markdown no longer describes. The one
exception is a page built from a template, which keeps the template’s banner.
Page templates
Section titled “Page templates”A page can be created from one of the site’s own page templates, so every page starts with the same sections, banner and web parts.
Set it for a single page in its front matter:
---title: Extensionstemplate: PageTemplate---Or for every page at once, with the pageTemplate
option in doctor.json:
{ "pageTemplate": "PageTemplate"}The value can be the template’s page title, its file name, or its page id — whichever you
have to hand. A template saved in SharePoint lives under SitePages/Templates/, and its file name is
usually not the same as its title, so all three work:
template: Documentation Template # the page titletemplate: Documentation-Template.aspx # the file name, as it appears in the URLtemplate: Documentation-Template # the file name without the extensiontemplate: 144 # the page idThe page title is matched exactly first, then the rest case-insensitively. List the templates of your site to see what it has:
m365 spo page template list --webUrl https://<tenant>.sharepoint.com/sites/<site> --output jsonRe-applying a template to existing pages
Section titled “Re-applying a template to existing pages”--reapplyTemplates, or "reapplyTemplates": true in doctor.json, lays a page out from its
template on every publish instead of only when the page is created:
- the template’s canvas becomes the page’s layout — its sections, and any web parts it carries;
- the page keeps its own banner, so it keeps its own title and header image. A banner stores the page title inside the web part, so taking the template’s would put the template’s title on every page using it;
- the page’s content goes into the first empty one-column section, or into a new section below
the template’s layout when there is none. A web part the template carries is never taken over,
not even a Markdown one:
Doctorcannot tell a slot you left for content from a web part you mean to show, so it assumes the latter and leaves it alone; - the page keeps its id, URL, history, comments and column values. Nothing is recreated.
The template is read once per run, however many pages use it.
A page that names a template keeps the template’s banner when its front matter has no header,
so the template stays in charge of how the top of the page looks. Give it a header and that is
applied, as on any other page. The page content still comes from the markdown as always.
The doctor-sample repository shows the setting in use.
Images and other assets
Section titled “Images and other assets”An image a page refers to is uploaded to the asset library (--library, Shared Documents by
default) and the reference in the page is rewritten to point at it.
- An image inside your content folder keeps the structure it has there.
guides/img/logo.pngis uploaded toguides/img/logo.pngin the library. It makes no difference whether--folderis written as./src,srcor an absolute path. - An image outside it —
../assets/logo.png, for instance, or anything else reached with..— goes under a sharedassetsfolder in the library, keeping its path relative to the folder you rundoctorfrom.../shared/brand/logo.pngbecomesassets/shared/brand/logo.png.
Author
Section titled “Author”Doctor can set the page’s author — SharePoint’s own Author column, which is what the page shows
as its byline and what people filter on in the pages library.
---title: Release notesauthor: 12---The value is the user’s site user ID: the id they have in this site’s user list. A user only
has one once they are a member of the site or have visited it, which is why the id means nothing on
another site. The Doctor Metadata VS Code extension picks one for you, or you
can look it up at https://<site>/_api/web/siteusers.
A UPN works too, for front matter written by hand:
author: john@contoso.comA UPN does not have to belong to the site already. Doctor asks SharePoint to resolve it against
the tenant before it writes anything, which adds the site user when the site has not seen them
before — the same thing setting the column would have done. What it gets back is the login name
SharePoint stored, which is what ends up on the page: that matters for guests and groups, whose
claims are not the i:0#.f|membership| shape a UPN is assembled into.
Any other value — a display name such as author: Jane Doe, which content written for another static
site generator often carries — is not taken for a user: Doctor warns and leaves the Author column
as it is, and the page is published.
If the id does not exist on the site, or the name does not exist in the tenant, the page is skipped — see below. For an id, the warning lists a few that do exist.
Metadata
Section titled “Metadata”Adding metadata for a page is done by specifying the metadata object with corresponding SharePoint field names and their values.
metadata: <field name>: <value>The field names support multiple formats for lookup:
- Internal Name (exact case-insensitive match)
- Static Name (case-insensitive match)
- Display Title (case-insensitive match)
Basic Example
Section titled “Basic Example”---title: Homeslug: home.aspxlayout: Articledescription: "The Doctor documentation homepage"
metadata: Category: "Choice 1" SingleLineText: "Single line of text value"---Supported Field Types
Section titled “Supported Field Types”Simple Fields
Section titled “Simple Fields”The following field types are passed through unchanged:
Text- Plain text valuesNote- Multi-line text valuesNumber- Numeric valuesCurrency- Currency valuesBoolean- Boolean values (true/false)Choice- Single choice fields (use the exact choice value)
metadata: Category: "Choice 1" Priority: 5 Budget: 10000 Approved: trueTaxonomy Fields (Managed Metadata)
Section titled “Taxonomy Fields (Managed Metadata)”Single Taxonomy Field (TaxonomyFieldType):
Input can be a simple string label, or an object with label and optional term GUID:
metadata: Department: "Finance"Or with explicit term GUID (prevents lookup):
metadata: Department: label: "Finance" termGuid: "550e8400-e29b-41d4-a716-446655440000"Multi Taxonomy Field (TaxonomyFieldTypeMulti):
Input can be an array of labels or objects:
metadata: Skills: - "C#" - "TypeScript" - label: "Azure" termGuid: "660e8400-e29b-41d4-a716-446655440000"When a term GUID is omitted, Doctor resolves the label against the column’s term set, reading the
term store through the site (_api/v2.1/termStore). Terms are read once per term set per run.
A few things are worth knowing:
-
Anchored columns. When the column is pinned to a sub-tree of its term set (an anchor term), only that sub-tree is searched — the same terms the column actually allows.
-
Synonyms work. A term can be written by any of its labels, not only the default one.
-
Duplicate labels. Term sets often reuse a label in different branches. Write the path to say which one is meant:
metadata:Region: "Regions > Europe" -
An unknown or ambiguous term skips the page — see below. The warning names the term and, when it is ambiguous, the paths it matched. Give an explicit
termGuidto bypass the lookup entirely. -
Deprecated terms are ignored, since SharePoint does not accept them on an item anyway. The terms under a deprecated term still can be used.
User Fields
Section titled “User Fields”Single User Field (User):
Input is a UPN (User Principal Name). Output is formatted as a SharePoint person claim:
metadata: Owner: "john.doe@contoso.com"Outputs: [{"Key":"i:0#.f|membership|john.doe@contoso.com"}]
Multi User Field (UserMulti):
Input is an array of UPNs:
metadata: Approvers: - "john.doe@contoso.com" - "jane.smith@contoso.com"Outputs: [{"Key":"i:0#.f|membership|john.doe@contoso.com"},{"Key":"i:0#.f|membership|jane.smith@contoso.com"}]
Every name on a person column is resolved against the tenant before the page is written, and the
login name SharePoint answers with is the one that ends up on the page — so a guest or a group gets
the claim SharePoint actually matches on, rather than one assembled from the name. A name the tenant
does not have is reported and its page is skipped, and that goes for the whole column: if one of the
Approvers cannot be resolved, none of them are written.
Resolving a name also adds the site user when the site has not seen them before, which is what setting the column would have done anyway. Each name costs one lookup per run however many pages use it.
DateTime Fields
Section titled “DateTime Fields”Input accepts multiple formats:
- Already normalized:
"2024-01-15 10:30:45"(passed through unchanged) - Date only:
"2024-01-15", or2024-01-15without quotes (midnight on that day) - ISO 8601 without a time zone:
"2024-01-15T10:30"(the date and time as written) - ISO 8601 with a time zone:
"2024-01-20T14:30:00Z"or"2024-01-20T16:30:00+02:00"(converted to the site’s time zone, by SharePoint)
metadata: PublishDate: "2024-01-15" ReviewDate: "2024-01-20T14:30:00Z" ApprovalDate: "2024-01-22 09:00:00"SharePoint reads the result in the site’s time zone. Doctor never uses the time zone of the machine it runs on for the formats above, so the same markdown publishes the same value from your laptop and from a pipeline. A value with a time zone names a moment, so Doctor asks the site what its clock read at that moment and writes that: "2024-01-20T14:30:00Z" becomes 15:30 on a site in Brussels. Quote such a value — written without quotes, YAML parses it before Doctor sees it and the zone is lost, so the time is taken as written. A value that is not a date — or not a real one, like 2024-02-30 — is reported as a problem and the page is skipped, rather than passed to SharePoint to be refused after the page has been written.
Lookup Fields
Section titled “Lookup Fields”Single Lookup Field (Lookup):
Input is a numeric item ID (integer or numeric string):
metadata: ParentPage: 42 ReferencedPage: "123"Multi Lookup Field (LookupMulti):
Input is an array of numeric IDs, joined with ;# delimiter:
metadata: RelatedPages: - 1 - 5 - 8An id has to be a whole number above zero. A value which is not one is reported as a problem and the page is skipped; in a list, one such entry is enough — see below.
URL Fields
Section titled “URL Fields”Input can be a string or an object:
metadata: # String format (url, description) CompanyWebsite: "https://contoso.com, Company Home"
# Object format with url and description MoreInfo: url: "https://contoso.com/docs" description: "Full Documentation"
# Object with url only Link: url: "https://contoso.com"An object without a url is reported as a problem, and the page is skipped.
Multi-Choice Fields
Section titled “Multi-Choice Fields”Input can be a pre-formatted string or an array of values, joined with ;#:
metadata: # Pre-formatted string Tags: "Tag1;#Tag2;#Tag3"
# Array format (automatically joined) Features: - "Feature A" - "Feature B" - "Feature C"Empty entries in a Choice array are left out, and a number or true/false is written as text — - 2026 is the choice 2026.
Every multi-value column takes the list as a whole: if one entry cannot be read — a choice which is a list or an object, a person
column entry which is not a user principal name, a lookup entry which is not an item id, a managed
metadata entry which is neither a label nor a { label, termGuid } pair — the column is reported as
a problem and the page is skipped. Doctor does not write the entries it could read and leave the
rest out, because that would put a shorter list on the page than the markdown asks for, without
saying so.
What happens when a value cannot be set
Section titled “What happens when a value cannot be set”Doctor works out every metadata value before it writes anything. If any of them cannot be
resolved — the column does not exist on the Site Pages library, a term is not in the term set, an
author is not a user of the site, a value is not one its column type accepts — the page is skipped
whole:
- a warning names the page and every problem found on it;
- nothing on that page is touched — its content, header and metadata stay exactly as they were, so it is never left with new content and stale metadata;
- it stays out of
.doctor/state.json, so the next run does not consider it unchanged and publishes it once the front matter is fixed; - the run carries on with the next page, and ends successfully.
Skipped pages are counted with the skipped pages in the summary. The warnings are listed at the end
of the run, and in the warnings array when using --output json.
What gets checked
Section titled “What gets checked”- The column has to exist on the Site Pages library.
Doctormatches on its internal name, its static name or its display name, case insensitively. A handful of internal names cannot be set because the CLI underneath reads them as its own options (webUrl,listId,id,contentType,outputand a few more); one of those is reported rather than sent. - The value has to be one its column type accepts — an item id for a lookup column, a user principal name for a person column, a term which is in the set for a managed metadata one.
- People are resolved against the tenant, which is also what adds the site user when the site has
not seen them before. A name the tenant says it does not have is a problem, and is remembered for
the rest of the run. A lookup that fails for another reason — a timeout, a throttle — is not an
answer about the name: it fails that page, and the next page asks again. If the account is not
allowed to look users up at all,
doctorsays so once and carries on without the check — an unknown name then fails its page while it is being written, instead of being reported before. - Terms are resolved against the column’s own term set, honouring its anchor term, the term’s
other labels and a
Parent > Childpath. A label which matches more than one term is reported as a problem rather than guessed at. - SharePoint still validates on its side. A value which passes these checks can still be refused by the library itself — that is a publishing error like any other, and it fails the page rather than skipping it.
None of this runs when the account is not allowed to set columns on the Site Pages library at all. In
that case the metadata and author front matter is skipped for every page, reported once, and the
pages themselves are published as normal — see
available permissions.
Example with Multiple Field Types
Section titled “Example with Multiple Field Types”---title: Product Documentationslug: product-guide.aspxdescription: "Complete product guide with metadata"
metadata: # Simple fields Priority: 1 Approved: true
# Taxonomy (single and multi) Category: "Documentation" Tags: - "Product" - "Guide"
# User fields Owner: "product-team@contoso.com" Reviewers: - "alice@contoso.com" - "bob@contoso.com"
# DateTime field PublishedDate: "2024-01-15"
# Lookup field ParentPage: 42
# URL field SourceRepository: url: "https://github.com/contoso/docs" description: "GitHub Repository"
# Multi-choice field Features: - "Search" - "Export" - "Print"---
Your page content here...