Skip to content

About

Generate, serve and publish websites

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Static Site Publisher

Usage

The source directory must contain _site_config.yml. A key that is not a config field is an error, including a key nested under social, graph, or license. Field names are kebab-case (named-windows, facsimiles-url). Pass that directory as the only positional argument:

./gradlew run --args="/path/to/source-directory"

--serve generates and leaves the HTTP server running (same as IntelliJ @main def generate()). --pretty-print rewrites authored TEI, DocBook, Markdown, and AsciiDoc in place and does not generate a site. It skips a file whose bytes would not change. It does not reformat HTML. --width=N is the prose column (default 120). --width=0 does not wrap. --sentence-per-line defaults to true. --sentence-per-line=false keeps a sentence on the same line and only wraps. XML ignores both flags. --pretty-print together with --serve is an error. bin/pretty-print reflows Markdown and AsciiDoc paths and does not need site_config.yml. It uses the extensions declared on those Markup instances. Fences, tables, headings, front matter, and AsciiDoc attributes are left as written. --log-level defaults to INFO. --treat-errors-as-warnings, --include-drafts, --production, and --target-directory-name are the other flags. SITE_PUBLISHER* environment variables are the same names with hyphens turned into underscores.

./gradlew build compiles and tests; ./gradlew test runs tests only.

Maven coordinates: org.podval.tools:org.podval.tools.publisher:0.9.0

Gradle plugin

A site with Gradle applies plugin id org.podval.tools.site-publisher (same version as the library). The plugin adds a detached sitePublisher configuration and three JavaExec tasks that run org.podval.tools.publish.site.Site. Do not put the library on implementation: Scala version and Playwright stay off the project compile classpath, and off the Gradle daemon.

plugins {
  id 'org.podval.tools.site-publisher' version '0.9.0'
}
site {
  treatErrorsAsWarnings = true
  logLevel = 'INFO'
}

pluginManagement.repositories must include mavenCentral() (the plugin marker is published there) as well as gradlePluginPortal() for other plugins such as Foojay.

./gradlew generateSite writes the target directory. ./gradlew serveSite is the same with --serve. ./gradlew prettyPrintSite passes --pretty-print, --width=N, and --sentence-per-line and does not publish _site. generateSite and serveSite pass --pretty-print=false and do not pass the prose flags. Do not make generateSite or prettyPrintSite a dependency of build; Pages CI calls generate explicitly. OpenTorah :docs keeps tasks.named('generateSite') { dependsOn generateTables }.

site { } maps to CLI flags (defaults match SiteOptions):

| Property | Default | CLI | |---|---|---| | sourceDirectory | project directory | positional source path | | targetDirectoryName | _site | --target-directory-name (absolute path used as-is) | | treatErrorsAsWarnings | false | --treat-errors-as-warnings | | logLevel | INFO | --log-level | | includeDrafts | false | --include-drafts | | production | false | --production | | prettyPrintWidth | 120 | --width=N (prettyPrintSite only) | | prettyPrintSentencePerLine | true | --sentence-per-line (prettyPrintSite only) |

The three tasks set --enable-native-access=ALL-UNNAMED, --sun-misc-unsafe-memory-access=allow, PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=1, and PLAYWRIGHT_BROWSERS_PATH=$GRADLE_USER_HOME/ms-playwright. generateSite launches Java 25. Its inputs are the source tree minus the target directory and Gradle build/; its output is the target directory, so Gradle up-to-date can skip a run when sources are unchanged. The generator still deletes and rewrites the target when the task runs.

The plugin adds the library as a default dependency of the sitePublisher configuration, at the same version as the plugin (org.podval.tools:org.podval.tools.publisher:0.9.0). Gradle defaultDependencies run only when that configuration is still empty, so a consumer can pick another published library version by declaring sitePublisher themselves (there is no site.publisherVersion). Do not add a second coordinate alongside the default — a non-empty configuration replaces the default entirely:

dependencies {
  sitePublisher "org.podval.tools:org.podval.tools.publisher:${providers.gradleProperty('sitePublisherVersion').get()}"
}

The supported pair is the same version as the plugin. An older library may not accept flags the plugin always passes; a newer one is usually fine. A local settings-body includeBuild of this repo substitutes the library module for whatever version string was requested.

Site workflows: checkout, setup-java 25, setup-gradle, cache ~/.gradle/ms-playwright, ./gradlew generateSite (or :docs:generateSite), upload _site / docs/_site, deploy Pages. Pin the plugin version in build.gradle. Known sites apply the plugin and call ./gradlew generateSite (or :docs:generateSite). GitHub Actions can still call dubinsky/site-publisher/.github/actions/generate@v0.1.0 for repos that do not apply the plugin (pin the action tag; optional version input overrides that action checkout’s gradle.properties).

Sites using this publisher:

Dogfooding an unreleased publisher

This repo already includeBuild`s sibling `../xml and ../../OpenTorah/opentorah.org when those checkouts exist (-PxmlDir= / -PopentorahDir=).

A site that applies org.podval.tools.site-publisher substitutes a local checkout with two includeBuild`s of the same directory: `pluginManagement (plugin id; pass a String, not a File) and the settings body (library child org.podval.tools:org.podval.tools.publisher). pluginManagement alone does not substitute the library now that it is not the included-build root. Do not use mavenLocal().

pluginManagement {
  repositories {
    mavenCentral()
    gradlePluginPortal()
  }
  final String publisherDir =
    providers.gradleProperty('sitePublisherDir').getOrElse('../site-publisher')
  if (file(publisherDir).isDirectory() && file("${publisherDir}/settings.gradle").isFile()) {
    includeBuild publisherDir
  }
}

// after plugins { }:
final File publisherDir = file(
  providers.gradleProperty('sitePublisherDir').getOrElse('../site-publisher')
)
if (publisherDir.isDirectory() && new File(publisherDir, 'settings.gradle').isFile()) {
  includeBuild(publisherDir)
}

Override with -PsitePublisherDir=. OpenTorah / MathWorlds defaults stay ../../Podval/site-publisher. When the directory is missing (CI), Gradle resolves the plugin and the library from Maven Central.

Without a site-side plugin, generate from this repo: ./gradlew run --args="/path/to/source-directory".

Publish

./gradlew nmcpZipAggregation
./gradlew publishAggregationToCentralPortal

The zip includes the library (org.podval.tools:org.podval.tools.publisher), the plugin implementation (org.podval.tools:site-publisher-plugin), and the plugin marker (org.podval.tools.site-publisher:org.podval.tools.site-publisher.gradle.plugin). Same version.

Site-header links are listed in _site_config.yml in display order (source path or site path, same resolution as home):

header-pages:
  - notes/index.md
  - posts
  - tags
  - graph

Omitted or empty means no header links (up/prev/next and format icons still appear). Unknown entries are recorded on the Errors page.

Optional facsimiles-url is the base URL of a facsimile JPEG site (trailing slash as in the collector tei@facsimilesUrl). Omitted means none. See Facsimiles.

facsimiles-url: https://storage.googleapis.com/facsimiles.alter-rebbe.org/

Optional tei-default-calendar is julian or gregorian (default gregorian). TEI date/@when numbers are in that calendar unless the element has calendar="#julian". See TEI dates.

tei-default-calendar: julian

Optional named-windows (default off) reuses four collector window names so collection, names, transcription, and facsimile can sit in separate tabs. Internal links target the destination viewer; each page sets window.name. Other sites should leave it off.

named-windows: true

license is one record. name is required. link, holder, and holder-link are optional. A blank field is an error, and holder-link requires holder. An omitted license key leaves out the head link and the footer line. A scalar license or a top-level license-link is rejected.

license:
  name: CC BY 4.0
  link: http://creativecommons.org/licenses/by/4.0/
  holder: the Open Torah Project
  holder-link: http://www.opentorah.org/

description stays plain text because it is also the Atom subtitle and the SEO fallback.

chumashquestions.org replaces the scalar keys:

license: CC by-nc-nd
license-link: http://creativecommons.org/licenses/by-nc-nd/4.0/

with:

license:
  name: CC by-nc-nd
  link: http://creativecommons.org/licenses/by-nc-nd/4.0/

Summary

This here is a static site generator. It:

Supported markup constructs:

Opinionated

Since I am writing this site generator for my own use, it is going to be opinionated:

  • everything is written in Scala

  • no plugins

  • no SCSS

  • no template languages

  • no layouts

TODO expound

Markup

Recognized by extension:

  • Markdown (.md)

  • AsciiDoc (.adoc)

  • HTML (.html)

  • TEI (.xml with a TEI root: TEI, store, collection, person, place, org)

  • DocBook (.xml with a DocBook root: article, book, chapter, appendix, part, set, preface, refentry, topic)

Frontmatter

Internal (YAML between --- lines in the markup file) or external (same basename, .yaml / .yml). Do not use both.

Obsidian treats internal YAML as Properties; it stores extra keys but does not interpret this publisher’s fields (chunk, bibliography, csl, …). Other keys are kept. modified_time sets the page modified time when its value is a date; any other value is ignored and the key stays.

Set title in front matter only when it differs from the file name. Both the file name and the title are used for link resolution. If the document also has a title (h1, AsciiDoc =, TEI/DocBook title), it must match the front matter title; a mismatch is recorded on the Errors page.

Useful fields:

  • chunk / chunk-depth — see Chunking

  • bibliography / csl / lang — see Bibliography

  • toc-depth

  • description, author, lang — page <head> (SEO); site values in _site_config.yml are the fallback. Site lang also selects store and selector display names (built-in terms, optional _terms.xml, TEI <name lang>); omitted is en. Generated chrome follows that lang in _site_config.yml, and English when it is omitted. That is the gear, the collection table, and the search page.

  • tags, categories (Categories), aliases

  • icon, permalink, post, date

  • pdf - see PDF

  • asset — copy the markup file as an asset (no dialect processing, no site chrome). Standalone sidecar only: name.yml / name.yaml next to name.html (or .md, .adoc, .xml, …) with asset: true. The sidecar is not published. Internal --- asset: true is an error (the file stays markup). A directory index so marked stays a directory page (parent/children) but write copies the file (no listing, no chrome).

permalink (absolute, e.g. /short) and aliases (relative or absolute) each create a Refresh page at that URL. A permalink that does not start with / is an error (/errors.html#permalink) and creates no alias. When the page is a directory (an index or a TEI store / collection beside its folder), /permalink/child and wiki [[permalink/child]] resolve to child under that directory — the short name is a path prefix, not only a leaf bookmark. A remainder under a non-directory alias does not resolve. A last-dot suffix in a href that is all digits is part of the name (255.2), not a file extension.

Categories

categories is a list of links to authored pages. A value is a wiki link ([[Books]] or [[Books|label]]) or a bare name (Books). It resolves the same way as a wiki link in the body. The target is a normal page: its prose stays, and the generator appends the pages that name it, sorted by title. A value that resolves to nothing is an unresolved link on the Errors page. No category page is created for it. ![[Note.base]] and ![[Note.base#View]] are Obsidian queries and are omitted, with no error. The member page header lists each category beside its tags. A wiki link in the body is a backlink. It does not put the page in that list. A page may name several categories.

Page Names

Jekyll relies on page titles being given explicitly in the front matter and ignores the file names; I had to use a TODO Obsidian plugin to set the titles from the file names to communicate them to Jekyll; as a result, pages where the title is different from the file name (e.g., index pages) had to be marked with TODO in their front matter to disable this plugin from setting the front matter title.

My plugin uses both the front matter title and the file name for link resolution, so I can remove this Obsidian plugin and redundant front matter titles which are the same as the file name, and set the title in the front matter only when it is different from the file name.

Obsidian uses file names exclusively and ignores the front matter title; for link resolution to use front matter titles also, TODO Obsidian plugin needs to be installed.

Wiki links ([[…]], including [[note|text]] and [[#id]]) are native Obsidian. Block ids: ^id at the end of a paragraph, or on its own line after a list, table, quote, or code block (blank line before the id). Link with [[note#^id]].

Obsidian embeds of pages (![[note]], ![[note#heading]], ![[note#^block]]) copy that authored region into the host (any markup on the target). The copy is a boxed aside.transclusion whose header is a link to the source (Notes Title or Notes Title § heading). The gear Settings checkbox Seamless transclusions hides the box and header (html.transclusion-clean). A missing heading or block is an error (not a silent whole-page embed). Footnotes inside a transcluded region are merged and numbered with the host. Media embeds (![[file.pdf]], images, audio, video) are unchanged. AsciiDoc include:: and TEI/DocBook xi:include are not this feature.

Wiki links in Obsidian:

  • are they case-sensitive?

  • do they take into account file name, title property, document title?

  • how do they look in the file if name reference is ambiguous?

  • can they be split between the lines?

  • do they support any kind of agglutination?

Entities

A TEI file whose root is person, place, or org is an entity; the kind is that root. The entity id is the file name without .xml (name/alter-rebbe.xml → alter-rebbe). The file may sit anywhere in the tree.

persName, placeName, and orgName with a non-empty @ref resolve to an entity when the kinds match and @ref is exactly that id (not a path, not the displayed name, not a title). A kind mismatch or unknown id is an unresolved internal link and is recorded on the Errors page. Without @ref, the name is left as-is (not a link) and is recorded on the Errors page (name without @ref, /errors.html#no-ref). That check runs on the first parse of TEI documents and of store/collection title / abstract / body — not on the name elements that define an entity file. TEI unclear is /errors.html#unclear. An entity file whose name is not the first name with spaces turned into underscores is /errors.html#misnamed-entity. Each kind on the Errors page is a fragment (AmbiguousTitle → #ambiguous-title). When more than one kind is present, a TOC links to those fragments.

<persName ref="alter-rebbe">Алтер Ребе</persName>
в <placeName ref="Вильна">Вильне</placeName>
<orgName ref="Виленский_кагал">виленский кагал</orgName>

Wiki [[alter-rebbe]] still finds the entity page by file name. Entity refs do not title-walk, so a markdown page titled alter-rebbe does not steal persName ref="alter-rebbe".

A TEI entityLists file is a catalog of buckets, not a store include list. Beside a directory (name.xml next to name/) it is that directory’s index (/name/index.html). Each listPerson, listPlace, or listOrg is a bucket:

  • the list kind is the element (listPerson → person files);

  • @n is the list id (/name/{n}.html when the catalog has two or more non-empty lists);

  • @role present: entities of that kind whose root @role matches, anywhere in the site; omitted: entities of that kind with no role;

  • <head> on the list is the list page title (<title> is still accepted). The heading is that title alone: no name selector word, and no axis line under it. A direct child <title> of entityLists is the catalog page title. ref and plural="yes" on entityLists name that title from the term’s plural instead of <title>. The two attributes are a pair, and they are not combined with a <title>.

Empty lists are omitted. Members are every matching entity on the site, sorted by file name; the link text is the first persName / placeName / orgName. The entity page <h1> / <title> is that first name, not the file name. Document backlinks on an entity page still come from @ref in other documents (grouped by TEI collection when the source sits under one). The group heading is that collection’s archive path, the same / spelling as the document breadcrumb, and the whole path is one link to the collection. The document row opens the first mention in the text. A mention that exists only in teiHeader leaves that row on the document, because the published page does not keep those anchors. The catalog itself is not a backlink source.

A place with location/geo is a point on /map.html. The page exists when any place has a parseable geo, including a site with no listPlace. The text is the latitude, then the longitude, separated by a space. idno with type="wikidata" may name the same place. The body is the figure: the drawing, its caption, and a table of the places that have a coordinate. A place without a coordinate is not on the page. The area of a circle is the number of cases that mention the place. A case is a collection whose parent groups by case or дело, and a case counts once. A place with a coordinate and no such case is a ring. A place too far from the others is listed in that table and is not drawn. A province, a country, or a part of a city is left without a coordinate. The place list does not draw the map, and the publisher does not link the list to the map. An optional markup file at map is prose above the figure. Its h1 or front-matter title replaces the page heading, Map or «Карта». The SVG accessible name stays the map.title string. The page is not a header link and not a directory-listing entry. /map is a leaf alias of /map.html. Rivers and the coast come from place-map-base.txt at the site root. The publisher does not supply them, and it does not check that the file exists. Without the file, the map is drawn with no such lines. The name starts with , so the file is not published. Do not add a _site_config.yml key for it.

A blank line separates polylines. A line is a comment. A block with no coordinates is ignored, so the attribution can stand in its own block. A block whose first line is coast is coastline. Any other block is a river. Each remaining line is longitude, then latitude. That order is the opposite of TEI geo.

To write the file for a site, give this prompt:

Write `_place-map-base.txt` at the root of this site-publisher site.

Read every place file.
A place with `location/geo` has a coordinate: latitude, then longitude, in decimal degrees.
Keep the cluster that fits in 15° of latitude and 15° of longitude.
Recompute the median after each drop.
Distance is the hypotenuse of the latitude difference and the longitude difference times the cosine of the median latitude.
Drop the farthest place until both spans are at most 15°.
That cluster is the region.
Leave the dropped places out of the box.

Rivers: Natural Earth 10m `ne_10m_rivers_lake_centerlines`.
Coast: Natural Earth 50m `ne_50m_coastline`.
Both are public domain.
The 50m rivers omit rivers the 10m set includes, so do not use them.
Clip to the cluster with about one degree of margin.
Simplify rivers to about 0.12° and the coast to about 0.15°.
Do not draw a line that is not in the extract.
A river or a shore that stops short in the extract stops short in the file.

Write `_place-map-base.txt` in this form.
A blank line separates polylines.
A line that starts with `#` is a comment.
A block with no coordinates is ignored, so name the source, the scale, and the public-domain status in a block of its own.
A block whose first line is `# coast` is coastline.
Any other block is a river.
Name the river on a `#` line when the extract names it.
Every coordinate line is longitude, then latitude, separated by a space.
That order is the opposite of TEI `geo`.

The leading `_` keeps the file out of the published site.
Do not add a key to `_site_config.yml`.

When two or more lists are non-empty, the catalog page is a TOC of those lists (links to the list pages; members are not inlined). A markup file at {n} is prose above that name-list page, for every kind. Its h1 or front-matter title replaces that page’s heading (<title> and l.store-current). The catalog TOC and the store-tree name stay the list <head>. Without that file, the <head> stays the heading. A catalog with a single non-empty list is that list (no {n} subpage). That same file is prose above the inlined list, and {n}.html is still not published. The catalog <title> stays the catalog title. Several entityLists files are independent catalogs.

The catalog directory is an identity prefix in collection-aliases.json (/name → /name/index.html, /name/{id} → /name/{id}.html), the same Worker mechanism as collection aliases. There is no synthetic /name.html dump.

<entityLists>
  <title>Имена</title>
  <listPerson n="jews" role="jew"><head>Жиды (они же Евреи)</head></listPerson>
  <listPerson n="unknown"><head>Неизвестно кто</head></listPerson>
  <listPlace n="places"><head>Места</head></listPlace>
</entityLists>

The same catalog titled from the built-in name term. On a Russian site the title is Имена. Each list keeps its <head>.

<entityLists ref="name" plural="yes">
  <listPerson n="jews" role="jew"><head>Жиды (они же Евреи)</head></listPerson>
</entityLists>

Directories

Root / is index.md / index.adoc, or a synthesized listing of that folder.

To open at a specific page instead, set home in _site_config.yml to an absolute site path. If the document is chunked, use the TOC (P/index.html):

home: /book/book/index.html

/index.html is then a meta Refresh to that page (not an HTTP 301). Do not also author index.md / index.adoc. Other root files stay on the site; they are just not listed at /.

Collection short names (collector site.xml <alias n="rgada" to="…"/>) are an optional alias on the TEI store / collection root, not site config and not front-matter permalink:

<collection n="3140" directory="3140" alias="rgada">

alias is the URL prefix (/rgada, /rgada/003). The publisher does not write a Refresh file at /rgada.html; Pages.find, emitted hrefs (including canonical/sitemap), and serve() rewrite /rgada/003 to the written …/3140/003.html. Inbound old collector facsimile URLs /rgada/facsimile/003 rewrite the same way (see Facsimiles). Front-matter permalink / aliases remain for leaf Refresh pages.

Generate also writes collection-aliases.json (segment from → collection directory to) for a Cloudflare Worker. The Worker is an internal rewrite (not a 301): longest slash-delimited prefix, then .html unless the request already has an extension (keep .xml); inbound /alias/facsimile/P becomes {to}/P/facsimile.html. Entity-lists catalogs are an identity prefix (/name → /name/index.html). Prefix routes (hostname/rgada*, hostname/name*, …) so assets skip the Worker. Deploy with .github/actions/deploy-alias-worker. Canonical entity hrefs stay the file path (/name/{id}.html when the file lives under name/).

A directory permalink (an index or TEI store/collection) is also a prefix: /short/child resolves under that directory, and emitted hrefs use the short path.

A TEI store file in the source root (archive.xml next to archive/) also gets two synthetic indexes, named from the file: {name}-collections.html (nested tree of child stores and collections) and {name}-index.html (flat list of descendant collections). The tree is titled Tree, or «Дерево» when lang is Russian. The nested list still labels each level with that store’s by/@selector word from the term (archive is архив when lang is ru, and archive otherwise). A store with ref and plural="yes" is headed by the sentence-cased lexicon plural (Books, Книги). The name stored for the URL is the lexicon plural (books, книги), not that heading. The flat list is titled Collections, or «Коллекции» when lang is Russian. A file at either path is rendered above the generated list. A title in that file, an h1 or front matter title, replaces the page title. The file is not listed in the root directory. Point header-pages at {name}-collections and home at /{name}-index.html when those should be chrome / the landing page.

The same root gets {name}-years.html when at least one original TEI document has that store as an ancestor. Documents are grouped by the year on profileDesc/creation/date. That year is the number written on the attribute, not a conversion to another calendar. The page is a closed outline. Each year is a collapsed line with the number of documents. Opening a year shows its months the same way. The month name follows the site lang. A month with no documents is omitted. A date with no month is a line after that year’s months: «без месяца» when lang is Russian, and "No month" otherwise. Opening a month lists the rows. When three or more documents share one exact day (@when with a day), that day is a line above those rows. In Russian the line is the day, the month in the genitive, and the count. In other languages it is the month name, the day, and the count. A same-year range with a month on both ends is listed under each month from the first through the last. A range inside one month is listed once, in that month. It joins a day line only when both ends are that same day. The row shows the date text when that text is not empty. One value and no text shows that raw attribute. Both ends in the same year and no text show the two raw values with an en dash between them. Different years show the text, an em dash, and both raw ends, or the two raw ends when there is no text. An unparsable value or a schematron conflict shows the text only, which may be empty. An inverted range, the end year before the start year, uses that same different-years label: the text, an em dash, and both raw ends when the text is not empty, otherwise the two raw ends joined by an en dash. It is listed under undated. A span of one year is listed under each year. A longer span is listed under the start year only. A document with no creation date, or a date with no temporal attribute, is listed under «Без даты» when site lang is Russian and under Undated otherwise. That line is closed and shows the count. Inside it, documents are grouped by the archive directly under the root store, each archive a closed line with a count, in the store’s include order. The section is omitted when every listed document has a year. A file named {base}-{xx} is not a row. The title is «Годы» when site lang is Russian and Years otherwise. A file at this path is rendered above the list. A title in that file replaces «Годы». The page is not added to header-pages. Each root store with original documents gets its own year file. A root with no original documents does not get a year page. One root store is left off the path text of its descendants: the breadcrumb and the other archive-path strings. There is no /collections or /years alias, with one root store or with several. A second root store puts that crumb back.

A TEI store or collection file beside a directory (dir.xml next to dir/) is that directory’s index. A collection index sets class="wide" on <html> so the content .wrapper is 1800px; header and footer stay the default width. Store indexes and documents under the collection do not. xi:include/@href (relative to the store file) is an ordered child reference, not XInclude: the target is not inlined. Store children are listed in include order (name: title). A collection body is table.collection-index (Описание, Дата, Кто, Кому, Язык, Документ, Страницы, Расшифровка), not a page list. Optional <part from="000"> title rows split the table; pageType is manuscript (default: 000 / 000об) or book (numeric). A file named {base}-{xx} (two-letter language, dash at length 3) is a translation of {base}. The two letters are the language code even when the catalog does not know them. It is not a table row. The Язык cell stays the code: he ru, with ru linking to the translation. The site header links every reading to the others. The link text is the autonym (русский), not [ru], and the link carries lang and hreflang. A translation links back. The same suffix works outside a collection. Prev/next still skip translations. A file named {base}+reg is a regularized reading of {base}, not a language. doc-ru+reg reads doc-ru. It keeps that file’s xml:lang. It is not a collection row. Prev/next skip it in every directory, and from the reading they are the original’s neighbors. It shares the original’s facsimile. The original’s header links [reg] after the language links. The reading does not link back, and it is not in the hreflang set. It is not in search. A site-root directory whose name is a catalog code (en, not English and not zz) is another reading of the tail with the same extension (en/notes/about.md and notes/about.md). col/en/001.xml is not one: only the first segment counts. A cluster needs the unsuffixed page that is not under a language prefix. A lone 001-ru.xml or a lone en/about.md has no header link and no hreflang. Two files with the same code are an error, and one link remains: the suffix file if there is one, otherwise the first published path. html lang is one stored code: text/@xml:lang, otherwise the filename suffix, otherwise that directory, otherwise front matter lang, otherwise site lang, otherwise en. Front matter, text/@xml:lang, text/@lang, the suffix, and the directory are signals. The site language is not. When two signals differ, the file is Wrong language. On a TEI file, a missing text/@xml:lang is a signal when a suffix or a language directory is present (missing != ru). Markdown and AsciiDoc have no such attribute, so en/about.md with no front matter is not an error. When the cluster has another member, every member’s head has hreflang for each member, including itself, and hreflang="x-default" pointing at the unsuffixed page. The URL is absolute, the same string as <link rel="canonical">. Canonical stays the page itself. A page with no alternate has no hreflang. There is no Accept-Language and no redirect. A name the catalog knows (Russian, ru-RU) is published as the code (ru). dir and data-interface-lang on <html> are the interface language, which is site lang, not html lang. dir is rtl for he and yi, and ltr otherwise. The root rules html[dir="ltr"] and html[dir="rtl"] set both direction and text-align, so a Hebrew document does not right-align the Russian chrome. An xml:lang on a content element is also an HTML lang on that element. <code lang> is the highlight language (tei-lang), not HTML lang. When the document language and the interface language differ, the site header and footer, the page or store header, the facsimile aside, backlinks, embedded-in, category members, page breaks, and the date tip carry the interface lang and dir, and div.page-body carries the document lang. A note’s h1.page-title carries the document language. On a collection document the value cells do, and l.store-current does not. A store title and a store body are wrapped so the selector word stays in the interface language. span.store-name keeps the language of the name. The page header (.store-header) holds one ancestor path (nav.store-path) and this node’s selector + name + title as an <l class="store-current">, then abstract/body and the by/@selector label. The steps are separated by /. The flat collection index, search hits, the year index, the citation, and sourceDesc use that same spelling. When the site has exactly one root store, that store is left off the path of its descendants. A second root store puts the crumb back. The root page itself is unchanged. A collection document with no title (no nonempty titleStmt/title, and no markup or front-matter title) does not get that large line. Its own selector and name (документ 071) are the last crumb, not a link, with aria-current="page". A document that has a title keeps the large line. A TEI document under a collection also gets a document-header table from teiHeader (Описание, Дата, Кто, Кому, Расшифровка, Перевод). A line is omitted when its value has no text. The Кому cell is the first persName in a correspAction with type="received". When no action has that type, it is the persName with role="addressee". An endnote in teiHeader is not an article footnote. A note in abstract, in the date, or in the addressee persName is a footnote on that header cell, and on the same cell of the collection index. The Дата cell (and in-text dates) get calendar hover tables; see TEI dates. Selector words (document → документ when site lang is ru) come from the built-in terms. A site adds axes in _terms.xml at the source root: a <Terms> file of <term id="…​" selector="yes">. by/@selector is that id. An unknown id fails generation. It is not an Errors-page warning. A store or collection with ref and plural="yes" takes its name from that term’s plural. Do not also write <name> or a root n. <title>, <abstract>, and <body> stay. The heading capitalizes that plural. An authored <name> is shown as stored. Documents under a store or collection use the same header. Each TEI document is also written as XML beside the HTML (/rgia529/071.xml, and the same stem for a {base}-{xx} translation). The file is generated. It is not the git file and it is not in the sitemap. publicationStmt uses the site url and the nested license (name, link, holder). sourceDesc is the archive path, plus facsimiles-url when that is set. langUsage is that document’s xml:lang. calendarDesc is tei-default-calendar when that is set. revisionDesc says the published file is generated and the file in git is the source. The publication date is the build’s HEAD commit time, omitted when the source directory is not in a git checkout. The document header links to that XML file. On the breadcrumb line, at the right, a TEI document also has the TEI link, the GitHub link, and a Cite button. Each of those has a tooltip: "Published TEI", "Source file on GitHub", and "Copy citation". The citation is not shown. The button copies the archive path, the page’s canonical URL, the transcriber (Расшифровка), and the license name and holder. There is no access date. When the build has a HEAD commit, the copied text then says Git hash and the full SHA. When origin is a GitHub repository, it then gives the file URL, the same URL as the GitHub link. The canonical URL is the same string as <link rel="canonical">: url from _site_config.yml plus the public path. A url with no scheme is not given one. Every authored page with a source file links to that file on GitHub (https://github.com/owner/name/blob/<branch>/…) when origin is a GitHub remote. The link is the branch tip, not the commit the citation quotes. git@github.com:owner/repo.git, ssh://git@github.com/owner/repo.git, and https://user:token@github.com/owner/repo.git all become https://github.com/owner/name. A token in the remote is not written into the page. A source directory below the work tree (docs/) is included in the path. If origin is missing or is not GitHub, there is no link. There is no repository key in _site_config.yml. A scanned file under that directory that is not named in the includes is an error (translations are not). Selector hops in the href (book/ in books/book/derzhavin.xml) are URL segments only — they are not pages, and up skips them. Above the table, one line gives the number of documents. Missing photographs, split into empty leaves and leaves with text, and the scanned leaves of a document that has no transcription, are on that line when the count is not zero. Each of those counts opens one link per leaf. Collection Страницы links to pb ids in the transcription (#p{n}); in-text pb links to the facsimile viewer when facsimiles-url is set.

  • directory structure

  • navigation

  • header pages

  • icons

TEI dates

A TEI <date> with @when, @from, @to, @notBefore, or @notAfter gets a hover table. .. in any of those attributes is InvalidDate, and the element is left as authored. Each value is YYYY, YYYY-MM, or YYYY-MM-DD. Month names follow the site lang. The column headers stay English.

A full day on @when is one column, Date. A year or a month on @when is both ends of that year or month. A year or a month on @from or @notBefore expands to the first day of that end only. A year or a month on @to or @notAfter expands to the last day of that end only. @from equal to @to, or @notBefore equal to @notAfter, stays two columns, not Date.

| What you write | Hover columns | |---|---| | @when on a full day (YYYY-MM-DD) | Date | | @when on a year or a month | From, To | | @from and @to | From, To | | @notBefore and @notAfter | Not before, Not after | | @from only | From | | @to only | To | | @notBefore only | Not before | | @notAfter only | Not after | | @from and @notAfter | From, Not after | | @notBefore and @to | Not before, To |

These combinations are InvalidDate, and the element is left as authored:

  • @when together with @notBefore, @notAfter, @from, or @to

  • @from together with @notBefore

  • @to together with @notAfter

@from with @notAfter, and @notBefore with @to, are allowed.

The numbers are Gregorian unless the element has calendar="#julian", or _site_config.yml has tei-default-calendar: julian and @calendar is omitted. Any other @calendar value is Gregorian. This disagrees with the TEI Guidelines: the attributes are not rewritten to Gregorian; @calendar selects how the numbers are read, and it still describes the prose.

Markdown uses the same element. A <date> in a Markdown page with those attributes gets this hover. The words on the page stay as written. @calendar and tei-default-calendar apply as they do for TEI. A <date> in code stays as written. A hover tip inside a heading is left out of the section id and the table-of-contents title.

The collection index and document-header Дата cell show @when as the visible text when @when is present. A note inside that date stays in the cell. Otherwise the cell shows the element’s own text, still inside <date>, so the tip can attach. In-text dates always keep their text. A <date> with none of @when, @from, @to, @notBefore, and @notAfter is unchanged.

TEI gap

A TEI <gap reason="…"> with a non-empty @reason gets a hover tip with that text (same family as dates). An empty element with a non-empty @reason shows one […] mark. @content is not consulted. Missing or blank @reason leaves the element as-is.

TEI hand

@hand="#id" points at a handNote in the same document. @medium on that note is pencil or ink. The note text is the hover tip. A pointer that is not a single #id of a handNote in the document is an error (/errors.html#unresolved-hand), and the element is left unwrapped.

<handNote xml:id="pencilHand" medium="pencil">записано карандашом</handNote>
<fw type="pageNum" place="right" hand="#pencilHand">6</fw>

TEI choice

Author an abbreviation as abbr then expan:

<choice><abbr>Wilen<hi rend="superscript">ch</hi></abbr><expan>Wilenskich</expan></choice>

The published tags are the same. There is no new element. orig and reg stay those names. The existing CSS draws ( before the second child and ) after it, so the line reads Wilen with ch superscript, then (Wilenskich). The expansion is italic. The parentheses stay roman. supplied stays in square brackets, and corr keeps the editorial background. The publisher does not reorder. Reverse order would show the expansion as the main text.

Abbreviation expansions and corrections is on unless the reader turns it off. Off hides expan, corr, and reg the same way: each leaves the line and comes back as a hover tip. A dotted underline marks an abbreviation that has no titlo. orig stays in the line, as abbr and sic do. sic keeps its editorial underline. It does not gain a dotted underline. A titlo stays the overline. Print and PDF show the expansion, the correction, and the regularization. The checkbox appears when the site has a choice. See Settings. It is not the apparatus menu below.

A titlo:

<choice><abbr rend="titlo">мца</abbr><expan>месяца</expan></choice>

The overline is abbr[rend~="titlo"]. A titlo with no expansion is abbr rend="titlo" and no choice, so there are no empty parentheses.

An abbreviation in the source is not supplied and not corr. supplied stays an editor addition in brackets, and corr stays a correction inside choice with sic.

Facsimiles

When _site_config.yml has facsimiles-url, each TEI document with at least one non-missing pb gets a viewer at P/facsimile.html (published /alias/P/facsimile.html when the collection has alias). Photos scroll in a pane under the header (the window is the size; there is no inner resize box). Old collector bookmarks /alias/facsimile/P (and /alias/facsimile/P.html) still resolve to that viewer, including translation /alias/facsimile/P-xx (shared original). #p{n} fragments are unchanged. New pages emit only the new URL.

The JPEG for pb@n is {facsimiles-url}{directory of the TEI file}/{n}.jpg. A trailing slash on the config value is optional. pb@facs overrides that URL. Missing pb@missing pages are omitted from the scroller.

In the transcription, <pb n="000-1"/> is an images icon and the leaf name 000 (a.pb, id="p000-1") linking to the viewer fragment, target="facsimile" (facsimileViewer when named-windows is on). The name is smaller than the transcription. The icon and the name stay on one line. If they do not fit in the space left on the line, both move to the next line. Hovering the mark shows the leaf name and «Факсимиле.». The site-header format icon does the same without a fragment. A missing photograph is its own line: the same icon faded, then the leaf name and «Нет фотографии.». An empty leaf adds «Пустая.». The line is not a link. Hovering it shows that same sentence.

On a wide window the transcription shows the current scan to the right of the text. The text column stays the same width. The gear row "Show the facsimile beside the text" is present when the site has a companion pane. See Settings. It is on unless the reader turns it off. A page-break icon selects that scan while the pane is open. It opens the viewer when the pane is closed. The header facsimile icon always opens the viewer. A page with no transcribed text keeps its scan in that pane. While the pane is open, that page is a screen tall, so scrolling reaches it. A missing photo does not. Print and PDF do not include the pane.

Collection Страницы is page numbers on the transcription #p{n} only. Each photo in the viewer links back to that pb in the transcription (target="text" / textViewer). {base}-{xx} translations share the original’s viewer.

No new _site_config.yml key. Omitted facsimiles-url still means no viewer and no pane; pb keeps its id (collection Страницы still works) and has no href.

Blog Posts

A page is a blog post in one of three ways:

  • A file in _posts/ or _drafts/ named YYYY-MM-DD-title is published at /YYYY/MM/DD/title.html.

  • Front matter post: true keeps the file path and adds a Refresh page at /YYYY/MM/DD/{file name}.html. date is required. post-title replaces the file name in that path.

  • Front matter permalink: /YYYY/MM/DD/title does the same for any source file name. The Refresh page is that permalink.

/posts lists each real post once. The Refresh file is not a second entry.

Front matter description is the post teaser: Atom <summary> and, when present, a p.post-excerpt on every /posts batch. Not auto-cut from the body. Site description is not used here (SEO still falls back to it).

Paging

The /posts listing only. In _site_config.yml:

paginate-posts: 10

Omitted or less than 1 is off. Page 1 stays /posts.html; further batches are /posts/2.html, /posts/3.html. A nav.pagination sits under the list.

SEO

No extra config. Each HTML page <head> gets a document title (Page | Site title; home is just the site title), meta name="generator" pointing at this publisher, description, author, canonical URL, Open Graph, a Twitter summary card, and JSON-LD (WebSite / BlogPosting / WebPage).

Taken from existing fields: site title, description, url, author, lang, social.twitter; page title, description, author, lang, date (and git/modified_time for modified). No image, Facebook, or webmaster keys.

Graph

Off by default. When _site_config.yml has graph.enabled: true, generate writes /graph.json and /graph.html. /graph.html is a pan/zoom map of canonical authored pages and the resolved internal links between them (Cytoscape.js, loaded only on that page). Click a vertex to open the page (named-windows targets apply when that option is on).

Vertices are authored pages with a source file: notes, posts, TEI documents, entities, stores, and entity-list catalogs. Aliases, chunks, facsimile viewers, PDFs, assets, and synthetics (/errors, /tags, /posts, …) are not vertices; links to those views count as links to the owning page. Edges come from existing backlinks (kind: link, including TEI @ref) and, unless turned off, transclusions (kind: transclude). include-transclusions defaults to true; omit it or set it explicitly. Unresolved links stay on the Errors page, not as graph nodes.

Do not name a collection @alias="graph": that path is reserved for the graph page. Do not name a collection @alias="search" when search is enabled: that name is reserved through installPrefixAlias, the same way @alias="graph" is reserved.

graph:
  enabled: true
  include-transclusions: true
  exclude-path-prefixes:
    - days

exclude-path-prefixes match the source path (days/2020-10-23.md, archive/lvia/…), not the published URL. Daily notes publish as /YYYY/MM/DD/index.html; TEI collection aliases publish as /lvia1799-2/…. A notes vault typically enables the graph and excludes the Obsidian daily-notes folder (days when that is the configured folder). A large TEI archive should leave the graph off, or exclude archive to keep only names and notes.

Add graph to header-pages if you want a header link (there is no automatic icon). Pipeline notes belong in the Site Publisher design note, not here.

Off by default. Omitted search does not write /search.html or /search.json, and pages do not get a header form. When _site_config.yml has search.enabled: true, generate writes /search.json and /search.html, and /search rewrites to that page (no Refresh file; the Worker table includes it). The alias does not rewrite /search.json. That file is the index search.js fetches. Every page then has a header form, immediately after the site title, that submits to /search. That form does not load search.js or search.json. Only /search.html loads the script.

The index is the diplomatic transcription: the spelling of the source, including ѣ, without stemming or modernized spelling. A sic reading is indexed. A corr reading is not. A line-break hyphen is kept, and the joined spelling is the next word. Борухо-<lb break="no"/>вичъ is stored as Борухо-вичъ Боруховичъ. Punctuation stays on the hyphenated word, so Борухо-вичъ, is followed by Боруховичъ and not by Боруховичъ,. Consecutive break="no" hyphens are one joined word: на-хо-дилось and находилось. A hyphen that is not immediately before such a break is not joined, including что-то and a hyphen before an ordinary lb. A maqaf (U+05BE) is not joined. Notes, name pages, and a collection’s title and abstract are included. Collection tables, non-collection stores, and entity-list catalogs are not. An empty query shows no hits. Year, archive, and language filters combine with the query. A record with no language stays visible when a language is selected. A record with no year or no archive is hidden when that filter is selected.

Do not author search.md or search.adoc when search is enabled. SearchPage is added before the scan, so that file collides in claimPage, not in installPrefixAlias. When search is off, search.md is an ordinary page. Do not name a collection @alias="search" when search is enabled. That name is reserved through installPrefixAlias, the same way @alias="graph" is reserved for the graph page.

exclude-path-prefixes use the same source-path test as the graph. A document excluded there is still listed on the year page. There is no 4 MB limit.

search:
  enabled: false
  exclude-path-prefixes: []

Settings

The gear menu on each page stores choices in the browser. A row is on every page of the site, or on none.

Color scheme is System, Light, or Dark. Nothing saved, and System, follow the operating system. Light and Dark stay as chosen. Print and PDF stay on the light palette. When the scheme resolves to dark, code blocks use the highlight.js dark theme and a mermaid diagram uses the same tokens as the page. The page graph uses the same colors.

In the text is the heading for term definitions and expansions. The heading is omitted when the site has neither.

Term definitions and citations inlines glossary and citation definitions.

Abbreviation expansions and corrections is on unless turned off. See TEI choice.

Seamless transclusions hides the box and header around a transcluded region. See Page Names.

Show the facsimile beside the text hides the companion pane. See Facsimiles.

Transclusions and the facsimile row stay ungrouped. That section is omitted when the site has neither.

Apparatus

AsciiDoc roles mark additions, sources, and deletions. Each kind is independent and stored in the browser gear menu.

[.add]#added words#
[.see]#a source#
[.del]#removed words#

Marked is the default: additions in brackets, sources in angle brackets at the small size, deletions struck through. Plain shows the words with no chrome. Hidden sets display: none. The words stay in the HTML. The gear lists Additions, then Deletions, then Sources. Marked is labeled [Marked], a struck-through Marked, and <Marked>. The Sources label stays at the small size.

[.add] is words the editor added, the same kind of words as a TEI supplied. A TEI add is a scribal insertion and is not this role. [.del] is words the editor takes out of the reading text. A TEI del is the scribe’s own strikeout and is not this role. [.see] is a source. The role names and the TEI names both stay.

Each group appears only when that role is used anywhere in the site. A site with [.add] and no [.del] or [.see] shows only Additions. See Settings.

Nesting two kinds needs the inner span unconstrained:

[.add]#before [.see]##inner## after#

A with spaces on both sides stays inside the span. A against the previous word closes it. # does not escape a hash. Use ##, #, or # for a literal hash.

Off by default. When _site_config.yml has check-links: true, generate requests each author-written http and https URL and lists failures on the Errors page (#broken-link).

check-links: true

Checked: links (a@href) and media (img / video / audio / source @src, PDF object@data), including links inside footnotes and bibliography entries. An absolute URL whose host is the site url is a spurious self-link and is not requested. mailto:, tel:, javascript:, and data: are not requested. URLs inside code, and URLs only reached by transcluding another page, are checked on the page that authors them. The same URL is requested once per generate. Each page that contains it gets one error.

The request is HEAD. On 403, 405, or 501 it is retried once with GET. Redirects are followed. A status of 400 or higher, or a timeout or connection failure, is a broken link. The error text includes the status or the failure message.

Footer social links, the license link, the holder link, and facsimile JPEGs are not checked. Generation still finishes. The Errors page is the report, as for unresolved links. Timeouts are 5 seconds to connect and 10 seconds per request.

Chunking

Split a document into per-section HTML pages (any markup, not only AsciiDoc).

chunk: true
chunk-depth: 2

P.html is the unsplit document (wiki lands there). The split view is P/index.html (P/ on a static host): preamble plus TOC, then P/Section.html for each chunk. File hrefs to P/index.html stay there; they are not rewritten to P.html. No directory listing is synthesized for P/.

When both views exist, the site header has icon links between them (list for chunked, file for one-page). Chunked TOC shows ancestors plus the current subtree; sibling branches are titles only.

Table of contents

A page with chunk: true or toc-depth gets a generated TOC if none is marked in the body. When the window is at least 1100px wide, that TOC is a sticky rail beside the title and the text. On a narrower window it stays at the top of the text. A click lands with the heading below the site header. A placeholder in the body is replaced with that TOC (the first one only) even without those keys; toc-depth still controls how deep it goes (default 2).

Markdown

Kramdown {:toc} on a list. The list (seed item included) is replaced. Ordered or unordered:

* this list is replaced with the TOC
{:toc}

## Alpha

or 1. seed / {:toc} for a numbered outline.

A paragraph whose only text is [TOC] (any case) is the same placeholder (Typora, GitLab). It must be a top-level paragraph: not a link ([TOC]: /url then [TOC]), not in a list, quote, or code span.

HTML

<div class="toc-placeholder"></div>.

AsciiDoc, TEI, and DocBook have no in-body placeholder; use chunk / toc-depth.

PDF

Front matter pdf: true prints P.html to P.pdf (Chromium). The site header on P.html (and on chunked pages, if any) gets a PDF icon linking to it. The site header is hidden in print, so the PDF itself does not include those icons.

Tables

Markdown tables via FlexMark Tables Extension (GFM; Obsidian supports them natively).

Markdown
| A | B |
|---|---|
| 1 | 2 |

AsciiDoc tables via Asciidoctor.

AsciiDoc
|===
| A | B

| 1 | 2
|===
TEI
<table>
  <row role="label"><cell>A</cell><cell>B</cell></row>
  <row><cell>1</cell><cell>2</cell></row>
</table>
DocBook

CALS row / entry (optional tgroup); morerows becomes rowspan.

<table>
  <tgroup cols="2">
    <thead><row><entry>A</entry><entry>B</entry></row></thead>
    <tbody><row><entry>1</entry><entry>2</entry></row></tbody>
  </tgroup>
</table>
HTML
<table>
  <thead><tr><th>A</th><th>B</th></tr></thead>
  <tbody><tr><td>1</td><td>2</td></tr></tbody>
</table>

Task Lists

Unchecked [ ] and checked [x] items in a list. The static site shows disabled checkboxes.

Markdown

GFM / FlexMark Task List Extension (Obsidian supports them natively):

- [ ] open
- [x] done
AsciiDoc

native checklists:

* [ ] open
* [x] done

DocBook has no native task-list markup.

HTML

the IR shape (already a task list; no conversion):

<ul class="task-list">
  <li class="task-list-item">
    <input type="checkbox" class="task-list-item-checkbox" disabled> open
  </li>
  <li class="task-list-item">
    <input type="checkbox" class="task-list-item-checkbox" disabled checked> done
  </li>
</ul>

Footnotes

Hovering a footnote number shows the note; the numbered list is at the end of the (chunk of the) page. A note written with no space after the preceding word or element stays adjacent in the HTML.

A marker in a table cell is published as a lowercase Latin letter (a, b, …, z, aa) with the list immediately under that table (div.table-with-notes). Markdown still defines those bodies at the end of the source file; the published page moves them. AsciiDoc [1] is written at the call site (including in a cell); the published page still lists those notes under the table. Collector collection-index and document-header tables stay in the page arabic series.

A footnote inside another footnote is one level of letters a, b at the end of the parent note. There is no nested hover tip: the outer tooltip shows inner letter markers, not a nested list. Markdown and AsciiDoc get nested notes only if the processor emits an inner stub. TEI note place="end" inside another place="end" does. place="foot" nests the same way. A note of either place inside the other is a nested letter of its own series.

TEI converts note place="foot" and note place="end". place="foot" is the source-footnote series (div.footnotes.source-footnotes), listed before the endnotes. A source marker is the text color and roman. place="end" is the encoder endnote series. An encoder marker is italic, in the violet note color. Danger red stays for a missing name, an unresolved citation, and a deletion. An unplaced note is unchanged. @n, when non-blank, is the marker at the call site and in the list. Order and fragment ids stay on the generated series. The same marker may appear on more than one note. Obsidian inline ^[…] footnotes are unsupported.

AsciiDoc

inline note, or a named definition reused with an empty macro: A [2] in a table cell is written at the call site; the published page lists it under the table.

See this footnote:[A note.]
and again footnote:fn[Same note.] later footnote:fn[].
Markdown

FlexMark: use [^id] in the text; define [^id]: later. Obsidian supports that form natively (Help: Footnotes). Obsidian also has inline ^[…] footnotes; this publisher does not. A [^id] in a table cell uses the same [^id]: body at the file end; the published page moves it under the table.

See this [^note] and again [^note].

[^note]: A note.
TEI

definition and use are the same element at the point of reference (@n, when non-blank, is the marker):

<p>See this<note place="end">A note.</note>.</p>

A non-blank @n is that marker. The next note with no @n is still numbered in series.

<p>See this<note place="end" n="*">A note.</note> and <note place="end">the next.</note>.</p>

A place="end" inside another place="end" is a nested letter on the parent note:

<p>See this<note place="end">Outer<note place="end">Inner.</note></note>.</p>
DocBook

<footnote> at the call site (xml:id optional; non-blank label is the same marker). Reuse with <footnoteref linkend="id"/>. A <footnote> inside another footnote’s <para> is nested the same way if the converter emits an inner stub.

<para>See this<footnote><para>A note.</para></footnote>.</para>
HTML

IR stubs (same footnote-correlation-id on the call site and the body):

<p>See this<span class="footnote-link" footnote-correlation-id="note"></span>.</p>
<span class="footnote" footnote-correlation-id="note">A note.</span>

Glossary

A glossary is a description list marked as such. Term links get hover definitions (print inlines them in parentheses).

AsciiDoc

[glossary] on the description list; term ids from . Reference a term with [id].

See <<posuk>>.

[glossary]
[[posuk]]posuk:: verse
Markdown

PHP Extra definition lists (https://michelf.ca/projects/php-markdown/extra/#def-list) via FlexMark Definition List Extension. Mark the list with {:.glossary} after it (Kramdown IAL, same family as Table of contents). Without that marker the list is not a glossary (no term ids, no tooltips). The marker also separates adjacent lists.

Obsidian does not render PHP Extra definition lists natively (Markdown Guide: Obsidian). Community plugin Pandoc Extended Markdown renders Pandoc/PHP Extra term / : definition lists. {:.glossary} is Kramdown IAL, not Obsidian; it shows as a paragraph in the note. [[#id]] heading links are native Obsidian.

+

See [[#posuk]].

posuk
: verse

{:.glossary}

Term id is the id on <dt> if present, otherwise the term text with spaces turned into hyphens (Alter Rebbe → Alter-Rebbe). Reference a term with [[#id]] or [text](#id).

A lone : after a Markdown term is not a reliable empty definition: FlexMark can take the next term as that definition. Write a real definition line, or omit the term.

TEI

<list type="gloss"> (or type="glossary") of <label> / <item>. Term id from xml:id on the label, else on the item, else the label text with spaces turned into hyphens. Reference a term with <ref target="#id"> or <term ref="#id">.

<p>See <ref target="#posuk">posuk</ref>.</p>
<list type="gloss">
  <label xml:id="posuk">posuk</label>
  <item>verse</item>
</list>
DocBook

<glosslist> (or <glossary>) of <glossentry> / <glossterm> / <glossdef>. Term id from xml:id on the entry, else the term, else the term text with spaces turned into hyphens. Reference a term with <link linkend="id">. <variablelist> is a description list, not a glossary.

<para>See <link linkend="posuk">posuk</link>.</para>
<glosslist>
  <glossentry xml:id="posuk">
    <glossterm>posuk</glossterm>
    <glossdef><para>verse</para></glossdef>
  </glossentry>
</glosslist>
HTML

the IR shape (class="glossary"; each term is class="glossary-item" with an id):

<p>See <a href="#posuk">posuk</a>.</p>
<div class="glossary">
  <div class="glossary-item" id="posuk">
    <dt>posuk</dt>
    <dd>verse</dd>
  </div>
</div>

Bibliography

Two kinds, per document (no site-wide file or style). They can appear in the same document.

External (.bib + CSL)

Front matter:

  • bibliography — path to a .bib file, relative to this source file (.bib files are not copied to the site; un-ignore with _site_ignore if you want one published)

  • csl — CSL style name from the bundled styles (for example apa, ieee)

  • lang — CSL locale; falls back to the site lang, then en-US

Both bibliography and csl are required. Only works cited on the page appear in the list. In-text cites become links to #bibl-{key} on the matching list entry (first key if a cite names several). Unknown keys are highlighted and recorded as a page error.

AsciiDoc

no asciidoctor-bibliography gem. Also cite:key[] / cite:key[p. 12] (standard inline-macro form).

- `cite:[knuth79]`, `cite:[knuth79, p. 12]`, `cite:[knuth79, lamport94]` — parenthetical
- `cite:knuth79[]`, `cite:knuth79[p. 12]` — same, standard inline-macro form
- `citenp:[lamport94]` — narrative (author in the sentence)
- `bibliography::[]` — list placeholder (otherwise the list is appended)
Markdown

Pandoc citation syntax (no extra FlexMark extension). me@host.com is left as an email. @key inside inline code or a fenced block stays as written. Obsidian does not format [@key] natively. Community plugin Pandoc Reference List recognizes Pandoc citekeys in the note (sidebar; optional inline). Citations inserts [@key] from a .bib file.

- `[@knuth79]`, `[@knuth79, p. 12]`, `[@knuth79; @lamport94]` — parenthetical
- `@knuth79` — narrative (`me@host.com` is left as an email)
- `@hand` inside inline code, or the same in a fenced block, is not a citation
- `[-@lamport94]` — suppress author
- `:::bibliography` — list placeholder
See [@knuth79] and [@knuth79, p. 12] and [@knuth79; @lamport94]
DocBook

<citation>key</citation> (optional locator after a comma). biblioref @linkend that is not a native entry id is the same. Empty <bibliography/> is the list placeholder (otherwise the list is appended).

<para>See <citation>knuth79</citation> and <citation>knuth79, p. 12</citation>.</para>
<bibliography/>
HTML

the IR shape:

<p>See
<span class="citation" data-mode="parenthetical"><span class="citation-item" data-key="knuth79"></span></span>
and
<span class="citation" data-mode="parenthetical"><span class="citation-item" data-key="knuth79" data-locator="p. 12"></span></span>.</p>
<div class="bibliography"></div>
TEI

@cRef on ref / ptr is the citeproc key. @n is an optional locator. Empty <div type="bibliography"/> is the list placeholder (otherwise the list is appended).

<p>See <ref cRef="knuth79"/> and <ptr cRef="lamport94" n="p. 12"/>.</p>
<div type="bibliography"/>

Internal (in-document list)

Authored entries stay on the page (not cited-only). Links to an entry id get a hover tip. Entry ids are as written (#knuth79); they do not use the #bibl- prefix of the CSL list, so the same key can be cited both ways in one document.

AsciiDoc

[bibliography] list with [[[id]]] / [[[id,xreftext]]] anchors; cite with [id].

See <<knuth79>> and <<lamport94>>.

[bibliography]
* [[[knuth79]]] Knuth, Donald E. _TeX and Metafont_. 1979.
* [[[lamport94,Lamport 94]]] Lamport, Leslie. _LaTeX_. 1994.
TEI

ref / ptr @target the xml:id of a bibl / biblStruct in listBibl (not in teiHeader). Empty ptr shows @n or the id. cit with a quote is a quote, not this. A bare @target that matches a listBibl id is treated as #id.

<p>See <ref target="#knuth79">Knuth 1979</ref>
and <ptr target="#lamport94"/>.</p>
<listBibl>
  <bibl xml:id="knuth79">Knuth, Donald E. <title>TeX and Metafont</title>. 1979.</bibl>
  <biblStruct xml:id="lamport94">
    <monogr><author>Lamport, Leslie</author><title>LaTeX</title></monogr>
  </biblStruct>
</listBibl>
DocBook

<bibliography> with biblioentry / bibliomixed; cite with <link linkend="id"> or <biblioref linkend="id"/> (empty biblioref shows @xreflabel or the id).

<para>See <link linkend="knuth79">Knuth 1979</link>
and <biblioref linkend="lamport94"/>.</para>
<bibliography>
  <biblioentry xml:id="knuth79">Knuth, Donald E. TeX and Metafont. 1979.</biblioentry>
  <bibliomixed xml:id="lamport94">Lamport, Leslie. LaTeX. 1994.</bibliomixed>
</bibliography>

Markdown has no native in-document bibliography list.

Code Blocks

Inline code and fenced blocks. A language class on <code> (language-scala, …) loads highlight.js; language-mermaid loads Pele instead.

Markdown

CommonMark inline code and fenced blocks. Obsidian supports both natively, including a language identifier (Help: Code). Fenced ```mermaid diagrams are native in Obsidian too (Help: Diagram); this publisher loads Pele for language-mermaid.

Use `map` in

```scala
xs.map(f)
```
AsciiDoc

inline code ; [source] listing (optional language).

Use `map` in

[source,scala]
----
xs.map(f)
----
TEI

<code> (tagdocs module) with @lang for the formal language (not xml:lang). A newline in the content is treated as a block and wrapped in <pre>. <eg> / <egXML> are not converted.

<p>Use <code lang="scala">map</code> in</p>
<code lang="scala">xs.map(f)
</code>
DocBook

<code> / <literal> inline; <programlisting> / <screen> / <literallayout> as a block. @language (not xml:lang) is the highlight.js tag.

<para>Use <code language="scala">map</code> in</para>
<programlisting language="scala">xs.map(f)
</programlisting>
HTML
<p>Use <code>map</code> in</p>
<pre><code class="language-scala">xs.map(f)
</code></pre>

Callouts

Numbered annotations on lines of verbatim (listing, literal, or source), plus a list of those annotations. Not a general footnote mechanism.

AsciiDoc

<1> (or <.> for automatic numbering) at the end of a line; repeat the numbers in a callout list below. Hide a marker in the raw source behind a line comment (// <1>, # <1>, <!--1-→).

[source,ruby]
----
require 'sinatra' (1)
----
<1> Library import
DocBook

<co> in a listing; <calloutlist> / <callout arearefs> below. @label on co is the marker number.

<programlisting>require 'sinatra'<co xml:id="co1" label="1"/></programlisting>
<calloutlist>
  <callout arearefs="co1"><para>Library import</para></callout>
</calloutlist>
HTML

the IR shape:

<pre><code>require 'sinatra' <span class="callout" data-value="1">1</span></code></pre>
<ol class="callout-list">
  <li>Library import</li>
</ol>

Admonitions

Typed notices (note, tip, warning, …). Not listing Callouts and not asides/sidebars.

Markdown

Obsidian core callouts (> [!type]; optional title; +/- to fold). No plugin.

> [!tip] Save time
> Use the shortcut.

> [!faq]- Hidden
> Secret.
AsciiDoc

NOTE: / TIP: / IMPORTANT: / CAUTION: / WARNING: (paragraph or [NOTE]==== block; optional .Title).

NOTE: Auxiliary information.

.Watch out
[WARNING]
====
Multi-paragraph body.
====
DocBook

note, tip, warning, caution, important (optional child title).

<note><title>Save time</title><para>Use the shortcut.</para></note>
HTML

the IR shape (details when folded):

<div class="admonition" data-type="tip">
  <div class="admonition-title">Save time</div>
  <p>Use the shortcut.</p>
</div>

Asides

Untyped auxiliary content next to the main flow. Not an admonition.

AsciiDoc

[sidebar] or a ** block (optional .Title).

.Optional Title
****
Auxiliary content.
****
Markdown

no native syntax; Obsidian has none. Write an HTML

block.

<aside>
<p>From Markdown.</p>
</aside>
DocBook

<sidebar> (optional child title).

<sidebar>
  <title>Optional Title</title>
  <para>Auxiliary content.</para>
</sidebar>
HTML

<aside> (IR adds class="aside" if missing); optional title:

<aside class="aside">
  <div class="aside-title">Optional Title</div>
  <p>Auxiliary content.</p>
</aside>

Quotes

Block quotations. Not Admonitions (> [!tip]) and not Asides.

AsciiDoc

[quote] (or a __ block); optional .Title; attribution and citation title as [quote, Author, Source].

.A title
[quote, Jefferson, Papers]
____
A little rebellion now and then is a good thing.
____
Markdown

a CommonMark blockquote (>). No attribution syntax (an em-dash line is still quote body). Obsidian > [!type] is an admonition, not a quote.

> A Markdown quotation.
DocBook

<blockquote> or <epigraph>; optional title and attribution. Inline <quote> stays <q>.

<blockquote>
  <title>A title</title>
  <para>A little rebellion now and then is a good thing.</para>
  <attribution>Jefferson</attribution>
</blockquote>
HTML

<blockquote> (IR adds class="quote" if missing); optional title and <footer> attribution (cite kept as-is):

<blockquote class="quote">
  <div class="quote-title">A title</div>
  <p>A little rebellion now and then is a good thing.</p>
  <footer class="quote-attribution">— Jefferson<br/><cite>Papers</cite></footer>
</blockquote>
TEI

<quote>; <cit> groups a quote with <bibl> (attribution). A <bibl> directly in <quote> is attribution too. <q> stays inline.

<cit>
  <quote>A TEI quotation.</quote>
  <bibl>Jefferson, <title>Papers</title></bibl>
</cit>

Strikethrough

Deleted or superseded text. HTML <del> (browser line-through).

Markdown

GFM ~text~ (Obsidian native; FlexMark strikethrough extension).

See ~~struck out~~ here.
AsciiDoc

text.

See [line-through]#obsolete phrase# here.
TEI

<del>.

<p>See <del>TEI struck</del>.</p>
DocBook

<emphasis role="strikethrough"> (or line-through). Default <emphasis> is <em>; role="bold" is <strong>.

<para>See <emphasis role="strikethrough">DocBook struck</emphasis>.</para>
HTML

<del> (IR); <s> and span/mark with class="line-through" become <del>:

<p>See <del>HTML struck</del>.</p>

Figures

A block image (or other graphic) with an optional caption. Inline images stay <img>. Local src values are resolved to a site path (/pixel.svg); a missing file is an error (missing asset) and the <img> gets unresolved-asset. Wiki ![[image]] also finds a unique file of that name anywhere in the source tree.

Markdown

a ![alt](src) on its own line; optional "title" is the caption. FlexMark does not turn Obsidian |WIDTH / |WIDTHxHEIGHT in the alt into img width/height (the size stays in alt). Obsidian ![[image]] embeds stay <img> (resolved later); |WIDTH or |WIDTHxHEIGHT on that wiki embed do become width/height.

![A square](pixel.svg "A Markdown figure")
![[pixel.svg|320]]
![[pixel.svg|320x240]]
AsciiDoc

block image:: (optional .Title). Inline src is not a figure. Width and height (image::file.png[alt, 320, 240] or width=320, height=240) are kept on the <img>.

.A figure caption
image::pixel.svg[A square]
TEI

<figure> with <graphic url> and optional <head> (caption). <graphic> becomes <img>.

<figure>
  <head>A TEI figure</head>
  <graphic url="pixel.svg"/>
</figure>
DocBook

<figure> (or <informalfigure>) with <imagedata fileref> and optional <title> (caption).

<figure>
  <title>A DocBook figure</title>
  <mediaobject>
    <imageobject>
      <imagedata fileref="pixel.svg"/>
    </imageobject>
  </mediaobject>
</figure>
HTML

<figure> (IR adds class="figure" if missing); optional <figcaption>:

<figure class="figure">
  <img src="pixel.svg" alt="A square"/>
  <figcaption class="figure-caption">HTML figure</figcaption>
</figure>

PDF embeds

A PDF file shown on the page. Not PDF generation of a page.

Markdown

Obsidian ![[file.pdf]]; optional #page=N and &height=H (pixels if H is digits). Alias ![[file.pdf|Title]] is the link label.

![[sample.pdf]]
AsciiDoc

no native block; write the HTML IR (passthrough ).

DocBook has no native PDF-embed markup.

HTML

<object type="application/pdf"> (IR wraps it) or the IR:

<div class="pdf-embed">
  <object data="sample.pdf" type="application/pdf" aria-label="sample.pdf">
    <a href="sample.pdf">Open PDF: sample.pdf</a>
  </object>
  <p class="pdf-embed-link"><a href="sample.pdf">Open PDF: sample.pdf</a></p>
</div>

The sibling link is always visible and is what remains in print (the inner viewer is hidden). data (and the fallback href) are resolved like other local assets; #page=N is kept.

Video

A local video file, or a YouTube/Vimeo player. Local files use HTML <video> (like <audio>). Local src is resolved like images; YouTube/Vimeo <iframe> URLs are left alone.

Markdown

Obsidian ![[file.mp4]] (also webm, ogv, m4v). Alias ![[file.mp4|Title]] is the fallback link label.

![[clip.mp4]]
AsciiDoc

video::file.mp4[] (optional .Title becomes a figure caption). YouTube/Vimeo: video::id[youtube] / video::id[vimeo].

video::clip.mp4[]
DocBook

<videodata fileref>. A YouTube/Vimeo URL becomes an iframe.

<mediaobject>
  <videoobject>
    <videodata fileref="clip.mp4"/>
  </videoobject>
</mediaobject>
HTML

<video src> (IR adds class="video" and controls); a YouTube/Vimeo <iframe> gets class="video-embed":

<video class="video" src="clip.mp4" controls="controls">
  <a href="clip.mp4">Open video: clip.mp4</a>
</video>

1. …
2. …

About

Generate, serve and publish websites

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages