This document specifies Docfx vnext versioning dev design.
-
Support expressing version constrains as moniker range.
In order to ease version configuration and content maintenance, moniker range enable the writer to associate more than one moniker with content, but without needing to list monikers.
-
Support moniker zone markdown syntax.
-
Support publishing multiple files with same sitePath but different monikerRange(mutually exclusive with others), where the appropriate file is selected by URL with query
view={moniker}
. -
All files will be built in one round, which will bring a better performance.
-
Support viewing page with specific version by query
?view={moniker}
. If the version is not existed, they should be redirected to some other version, but not get a404
page. -
Support selecting Toc version in available versions.
-
Prevent user to visit a blank page in different version.
-
Ensure TOC always only show available (non-empty) page node under specific version.
-
Not support version validation on reference, including link and xref.
In phase 1, we will not check whether the version exists on the referenced file.
-
If user reference another file by link/Uid without query string
?view={moniker}
, we will not check whether the referenced file have the same monikerRange with current file. So if the referenced file have different monikerRange, the final view page is not guaranteed.Sample:
|- articles/ | |- folder1/ | | |- a.md | |- folder2/ | | |- b.md | |- folder3/ | |- b.md |- config.yml
the content of
config.yml
isname: dotnet files: "articles/**/*.md" monikerRange: "articles/folder1/**/*.md": "netcore-1.0" "articles/folder2/**/*.md": "netcore-1.0" "articles/folder3/**/*.md": "netcore-2.0" routing: "articles/folder1/": "articles/" "articles/folder2/": "articles/" "articles/folder3/": "articles/" monikerDefinition: "https://api.docs.com/monikers/"
If content writer want to reference
folder3/b.md
infolder1/a.md
by link'[B](../folder3/b.md)'
, when the reader click the link B on page{host}/{docset-base-path}/articles/a?view=netcore-1.0
, they will jump to page{host}/{docset-base-path}/articles/b?view=netcore-1.0
, which is the output of filefolder2/b.md
. -
If user reference another file by link/Uid with query string
?view={moniker}
, we will not check whether the referenced file contains thismoniker
, the query string will be preserved to the resolve result, but the final view page is not guaranteed.Sample:
|- articles/ | |- folder1/ | | |- a.md | |- folder2/ | | |- b.md | |- folder3/ | |- b.md |- config.yml
the content of
config.yml
isname: dotnet files: "articles/**/*.md" monikerRange: "articles/folder1/**/*.md": "netcore-1.0" "articles/folder2/**/*.md": "netcore-1.0" "articles/folder3/**/*.md": "netcore-2.0" routing: "articles/folder1/": "articles/" "articles/folder2/": "articles/" "articles/folder3/": "articles/" monikerDefinition: "https://api.docs.com/monikers/"
If content writer want to reference
folder3/b.md
infolder1/a.md
by link'[B](../folder2/b.md?view=netcore-2.0)'
, when the reader click the link B on page{host}/{docset-base-path}/articles/a?view=netcore-1.0
, they will jump to page{host}/{docset-base-path}/articles/b?view=netcore-2.0
, which is the output of filefolder3/b.md
.
-
-
Not support doing bookmark validation inside different versioned zones.
Sample: if there is two conceptual file
articles/a.md
--- monikerRange: netcore-1.0 || netcore-2.0 --- ::: moniker range="netcore-1.0" ## heading1 ::: moniker-end ::: moniker range="netcore-2.0" ## heading2 ::: moniker-end
If there is a link
'a#heading2'
inarticles/b.md
with monikernetcore-1.0
, when user click the link, the bookmark#heading2
will not work, but no warning will be reported in Phase 1. -
Not support generating static website. In phase 1, since docfx doesn't handle fallback case, there have to be hosting server.
-
Not support setting monikerRange of node in TOC file.
name: dotnet
files: "articles/**/*.md"
monikerRange:
"articles/v1.0/**/*.md": "netcore-1.0"
"articles/v1.0/**/*.md": "netcore-1.1"
"articles/v2.0/**/*.md": ">= netcore-1.0"
"articles/v2.0/sub/**/*.md": ">= netcore-1.0 < netcore-3.0"
routing:
"articles/v1.0/": "articles/"
"articles/v2.0/": "articles/"
monikerDefinition: "https://api.docs.com/monikers/"
-
content
contains all the articles to build, including all version. -
monikerRange
defines the moniker range expression in the file layer. Key is glob pattern to match files, Value is the monikerRange expression.The Key is not required to be mutually exclusive from others, we try to match glob patterns from bottom to top, and take the value of first match. The Value is not required to be mutually exclusive from others. (In DocFX v2, group's monikerRange is required to be mutually exclusive from others) If a file does not match any global pattern, no versioning information will be supplied to this file.
With this config:
- All markdown files under folder
articles/v1.0/
will have the moniker rangenetcore-1.1
- All markdown files under folder
articles/v2.0/sub/
will have moniker range>= netcore-1.0 < netcore-3.0
. - All markdown files under folder
articles/v2.0/
but not underarticles/v2.0/sub/
will have moniker range>= netcore-1.0
. - All markdown files under folder
articles/
but not in folderarticles/v1.0
andarticles/v2.0
have no version.
- All markdown files under folder
-
routing
defines the relationship between the relative folder path and the relative URL base path. Relative folder path is the local folder path relative to the config file, while relative URL base path is the partial URL base path following the base URL for current docset.For example, with the following docset:
|- articles/ | |- v1.0/ | | |- a.md | |- v2.0/ | |- a.md |- config.yml
with routing
"articles/v1.0/": "articles/"
, filearticles/v1.0/a.md
will be published to URL{host}/{docset-base-path}/articles/a
, and with routing"articles/v2.0/": "articles/"
, filearticles/v2.0/a.md
will also be published to URL{host}/{docset-base-path}/articles/a
.To better illustrate scenarios, in below sections, we call URL without
{host}
and{docset-base-path}
as the SitePath of the file. In our example, SitePath forarticles/v1.0/a.md
andarticles/v2.0/a.md
are the same:articles/a
.If in one round of build, different files with the same SitePath are included, if there are two files without
monikerRange
or the intersection of any two files' moniker list is not empty, an error throws. For example,articles/v1.0/a.md
has monikersv1.0, v2.0
whilearticles/v2.0/a.md
has monikersv2.0, v3.0
, an error throws as for versionv2.0
, the result is nondeterministic. -
monikerDefinition
is an API, which provide the definition for moniker list, both file and http(s) URI schemas are supported. The moniker definition defines the moniker name, product name, order and other metadata for moniker.A better user experience sample when using the new config:
If there is a file
articles/folder1/a.md
with monikerRange'netcore-1.0'
, and filearticles/folder2/b.md
with monikerRange'netcore-2.0'
, and you have to add a filec.md
with monikerRange'netcore-1.0 || netcore-2.0'
.In docfx v2, since you cannot have overlap monikerRange in config, you have to copy
c.md
toarticles/folder1/
andarticles/folder2/
, which will bring you more maintenance cost forc.md
.But in docfx v3, you just need to maintenance one
c.md
For conceptual markdown file, file level monikerRange setting is supported, user can set the file level monikerRange in GlobalMetadata
and FileMetadata
in config or YAML header.
---
monikerRange: >=netcore-1.0
---
Note
The final file level moniker range is the intersection of moniker range from config file
and moniker range from fileMetadata, if the intersection is empty, a warning will be logged.
Note
The config file
level moniker range should be defined to enable versioning. If moniker range is not defined in config file
, but Yaml header
moniker range is defined, a warning will be logged.
Inside the file, with help of versioned zone syntax, the file has ability to wrap versioned content into different zones.
---
monikerRange: >=netcore-1.0
---
# Moniker Zone sample
content in `>=netcore-1.0`
::: moniker range="netcore-1.0"
content just in `netcore-1.0`
::: moniker-end
::: moniker range=">netcore-1.0"
content just in `>netcore-1.0`
::: moniker-end
Note
The final zone moniker range is the intersection of file level moniker range and moniker range of this zone. If the intersection is empty, a warning will be logged.
Note
The config file
level moniker range should be defined to enable versioning. If moniker range is not defined in config file
, but moniker zone syntax has been used, a warning will be logged.
In phase 1, content writer is allowed to reference another file with specific version by query string ?view={moniker}
, and the query string will be preserved to the resolve result, but we will not do the version existed check, and DHS will handle the fallback logic.
There are several scenarios:
Reference type | Resolve result | Validation |
---|---|---|
relative link without version query - [B](b.md) |
href without query - <a href="b"> |
file existed check, do not check whether monikerRange of target file is same with current file |
relative link with version query - [B](b.md?view=netcore-1.0) |
href with original query - <a href="b?view=netcore-1.0"> |
file existed check, do not check whether version exists in target file |
absolute link/external link without version query - [B](/b.md) |
href without query - <a href="/b"> |
no validation |
absolute link/external link with version query - [B](/b.md?view=netcore-1.0) |
href with original query - <a href="/b?view=netcore-1.0"> |
no validation |
internal/external xref without version query - <xref: b> |
href without query - <a href="b"> |
uid existed check, do not check whether monikerRange of uid is same with current file |
internal/external xref with version query - <xref: b?view=netcore-1.0> |
href with original query - <a href="b?view=netcore-1.0"> |
uid existed check, and check whether the specific uid version exists |
In Restore step, moniker definition file will be restored to local storage, which will be used to evaluate monikerRange expression.
An moniker list is provided by moniker definition file restored from monikerDefinition
, the file structure should be:
{
"monikers": [
{
"moniker": "{monikerName}",
"product": "{productName}",
"order": "{order}",
"product_family": "{productFamilyName}",
"platform": "{platform}",
"display_name": "{displayName}"
},
...
]
}
-
moniker
is an unique identifier of this moniker. If two moniker define the samemonikerName
, an error throws. -
product
defines the product this moniker belongs to, and the operators in monikerRange will be interpreted inside the product it belongs to. -
order
defines the moniker order inside the current product, higher number takes the precedence, default value is 0. -
product_family
andplatform
is the attributes of this product, which is used by portal to manage the monikers. -
display_name
is used by the template to display this product on docs page.
For dynamic, the output path shares the same schema:
{output-dir}/{siteBasePath}/{monikerListHash}?/{site-path-relative-to-base-path}
siteBasePath monikerListHash site-path-relative-to-base-path
|--^-| |------------^------------------| |----------------^----------|
_site/dotnet/ecc061f43156f37be077db42abf8301a/api/system.string/index.html
?
means optional. When the file have no version, the output path will be{output-dir}/{siteBasePath}/{site-path-relative-to-base-path}
monikerListHash
is the first 32 characters of the hash(SHA-256, HEX) of this file's final moniker list, joined by comma.
site-path-relative-to-base-path
means the relative path of sitePath related to siteBasePath
- Content of conceptual file's output(
*.raw.page.json
)
{
...,
"monikers":
[
"moniker1",
...
]
...,
}
- Content for redirection file(
*.raw.page.json
)
{
"outputRootRelativePath": "../",
"rawMetadata": {
"locale": "en-us",
"redirect_url": "{redirectURL}",
"monikers":
[
"moniker1",
...
]
},
"themesRelativePathToOutputRoot": "_themes/"
}
- Content of Toc file's output(
toc.json
)
{
"items": [
{
"toc_title": "title",
"monikers": [
"moniker1",
...
],
"href": "{href}",
"children": [
{
"toc_title": "title",
"monikers": [
"moniker1",
...
],
"href": "{href}",
"children": [...]
}
]
}
...,
],
"metadata": {
"monikers": [
"moniker1",
...
]
}
...
}
- Content for
.manifest.json
{
"groups":{
"{groupId}": {
"monikers": [
"moniker1",
...
],
...
},
...
},
"files":[
{
"siteUrl": "{SitePath}",
"outputPath": "{outputPath}",
"sourcePath": "{sourcePath}",
"group": "{groupId}"
},
]
}
groupid
is the first 32 characters of the hash(SHA-256, HEX) of the monikers joined by comma.
Sample:
|- articles/
| |- v1.0/
| | |- a.md
| |- v2.0/
| |- a.md
|- config.yml
Then content of config.yml
is:
name: dotnet
files: "articles/**/*.md"
monikerRange:
"articles/v1.0/**/*.md": "netcore-1.0 || netcore-1.1"
"articles/v2.0/**/*.md": "netcore-2.0"
routing:
"articles/v1.0/": "articles/"
"articles/v2.0/": "articles/"
monikerDefinition: "https://api.docs.com/monikers/"
redirections:
articles/v1.0/old/a: /articles/a
redirectionsWithoutId:
articles/v2.0/old/a: /articles/a
The query view={moniker}
will be appended to the redirect_url
returned from hosting.
To make every version of redirection file redirect to corresponding target file version, content writer have to add multiple redirection rules which redirect to the same target URL, like articles/v1.0/old/a: /articles/a
(redirections) and articles/v2.0/old/a: /articles/a
(redirectionsWithoutId), and there is at most one rule set as withDocumentId.
Note
For redirection file with versioning, BI team should consider to use document_version_independent_id
instead of document_id
.
As a writer, I can put my document under a folder that belongs to moniker range >= netcore-1.0 <= netcore 4.0
. However, my document only contains netcore-1.0
, netcore-2.0
zone content but not any shared content. For end user, I don't want him/her to see the blank page under netcore-3.0
or netcore-4.0
. I want him to see the content of netcore-2.0
as fallback. Besides, I don't want toc show my article node under netcore-3.0
and netcore-4.0
monikers.
Sample:
---
monikerRange: netcore-1.0 || netcore-2.0
---
::: moniker range="netcore-1.0"
content in netcore-1.0
::: moniker-end
In this case, there is no content under moniker netcore-2.0
, when user view this page with ?view=netcore-2.0
, they should be fallback to netcore-1.0
but not getting a empty page.
To handle this case, we have to:
- Remove this moniker from the moniker range from this file's build result, and add it to the
blank_page_monikers
attributes. - Remove this moniker from the moniker range of this file in the Toc, and add it to the
blank_page_monikers
attributes.
For now,
blank_page_monikers
is just used for easily trouble shouting, it will not be consumed by DHS, but we will keep this attribute for the future using.
After doing this,
- This file will be hidden from the Toc when they select the moniker of Toc as
netcore-2.0
. - If user access URL
{page_url}?view=netcore-2.0
, the DHS will fallback to{page_url}?netcore-1.0
Moniker range itself cannot be interpreted, but within an ordered moniker list, moniker range can be simply evaluated to a list of monikers.
In phase 1, link will be resolved to relative sitePath without version info, DHS will handle the fallback behavior if the referenced page doesn't the version of current page.
In v2, each group will build one round, and in each group, there should be not be two files with the same uid, so the xref can be resolved correctly. But in v3, there is no group, all files will be built in one round, so there will be two file with same uid but different version, which is not acceptable, and we have to handle this case. To handle this, when we are generating the xref map for internal using, we have to contains the monikers of each uid.
Note
In phase 1, we don't export moniker information in output xrefMap.
If in one round of build, different files with the same Uid are included:
- When the files do not have
monikerRange
option set, order them by output site url, take the first uid, log a warning. - When the files have
monikerRange
option set, but have different SitePath, an error throws. - When the files have
monikerRange
option set, and have the same SitePath, these file are considered as different version of the same Uid, this is allowed. And when refer to this uid without moniker, the latest one will be picked, i.e: prefer (netcore-2.0, netcore-2.1) to (netcore-1.0, netcore-1.1)
In current docfx v3, Toc file is build at the same time with page
files, because they don't depend on the build result of those files, but consider of files with version information, the nodes in Toc files build result will contains monikers
attribute, which depends on the build result of those nodes, so we have to move Toc building to the post-build step.
When build the toc file(including the toc file included in another), we have to cover:
- Monikers of every node in toc(We don't support setting monikers of node in toc file) should be the union of current node's monikers and the monikers of all children. The moniker of current node should be:
- If the node is a text node(
## Header
), the monikers of current node is empty. - If the node is a relative link node(
## [Header](a.md)
) and the referenced node is a versioning page, the monikers of current node is the monikers of referenced page. - If the node is a relative link node(
## [Header](a.md)
) and the referenced node is a non-versioning page, the monikers of current node is null. - If the node is a absolute link node(
## [Header](/a)
or## [Header](http://test)
), the monikers of this node is the monikers of current node is null. - If the node is a relative folder node(
## [Header](a/)
), the monikers of current node is the monikers of the monikers of the document chosen as the resolved result of the current node(specified by the topicHref or the first node of the toc file under foldera/
). - If the node is a toc referenced node(
## [Header](a/toc.yml)
), the monikers of current node is the monikers of the document chosen as the resolved result of the current node(specified by the topicHref).
- If the node is a text node(
Notes
- When the monikers of toc node is null, this node will always display.
- When calculating the union of nodes' monikers, once there is a child whose monikers is null, the union result is null.
- Monikers of toc file - Intersection of the monikerRange setting in the config and file metadata(If the intersection is empty, a warning will be logged).
For now, if the monikers of the node is out of the moniker Range of this toc, we do not report warning.
In phase 1, when resolving the toc_rel
of each file, we still take the nearest toc file.
For redirection file, the output file also contains monikers
information.
To support versioning, config migration tool have to:
- Generate
monikerRange
config item according to the original group config in thedocfx.json
. - Generate
routes
config item according to the original group config in thedocfx.json
- Support publishing to with overlapping monikerRange.
- Add moniker definition API and token to global config so it can be restored.
- Generate publish manifest file according to the new data contract.
- Support migrating repository with versioning in migration tool.
-
To support publishing multiple files with different version to same sitePath, we have to set two configuration
monikerRange
androuting
, there are some duplicate information, can we simply the config? -
For now, hosting does not append current
query
andfragment
to the redirect URL, so content writer have to specific the those information in the redirect URL itself, can we improve this? -
In Docfx v2, the file level monikerRange is the intersection of the monikerRange from
config
andyaml header
, should we make the monikerRange fromyaml header
higher priority, so it can overwrite the monikerRange fromconfig
.
In phase2, we are going to support:
-
Cross version reference validation
User can reference to a file/Uid with specific version, and a warning should be reported if the file/Uid does not contains this version.
-
External uid reference with version.
-
Bookmark validation with moniker info.
- Be able to link to a file in the same group by relative path with query
?view={moniker}
, a warning should be reported if the file does not contains this moniker. - Be able to link to a file in different group by relative path with query
?view={moniker}
, a warning should be reported if the file does not contains this moniker. - Be able to reference a internal Uid with query
?view={moniker}
, a warning should be reported if the Uid does not contains this moniker. - Be able to reference a external Uid with query
?view={moniker}
, a warning should be reported if the Uid does not contains this moniker.
In phase 3, we are going to support:
- Generating pure
static
website.