Skip to content

Element metadata in XML and HTML

Some documents keep their metadata in attributes. Many keep it in elements. An XML article has a <title> child. A DITA topic has a full <prolog>. An HTML page’s <title> may be the only place the page names itself. docmeta reads both.

A value held in an element is keyed <immediate parent>.<element name>. The containing element is the namespace.

<article><byline>Ada Lovelace</byline></article> → article.byline
<prolog><author>Ada Lovelace</author></prolog> → prolog.author
<critdates><created date="2026-01-15"/></critdates> → critdates.created
<head><title>Docs</title></head> → head.title

Existing flat keys do not move. Root attributes, <meta> tags, <othermeta> entries and HTML’s <title> keep the names they have always had. A document can carry the same fact in two places, and both are validated. Neither wins. A value that wins silently discards the other, and the discarded one is exactly the one nobody is checking.

Format Region Yields
Generic XML the root’s direct text-bearing children <root>.<child>
HTML text-bearing <head> children head.title
DITA topic <prolog> and its nested containers prolog.*, critdates.*, metadata.*, prodinfo.*
DITA map <topicmeta> and its nested containers topicmeta.*, critdates.*, …

Two exclusions keep the key set honest, and both are why the convention can work without a hardcoded ignore list:

  • An element with element children is structure, not a value. <body> full of <p> is a container. Lifting it would concatenate a document’s prose into one key.
  • Whitespace-only text is indentation, not content. Every container in an indented file holds a newline and some spaces.

HTML additionally skips <script> and <style>: both are text-bearing and neither is metadata. Void elements such as <meta>, <link> and <base> hold their value in an attribute, and are not lifted by convention. <link> repeats, and collapsing a canonical URL together with three stylesheets would discard the rel that told them apart. Reach those with a config path.

Lists, and why generic XML always uses one

Section titled “Lists, and why generic XML always uses one”

Plain XML states no cardinality, so docmeta cannot know whether a second <tag> is coming. A type that changed with document content could not be written against, so the convention commits to a list:

"article.title": { "type": "array", "items": { "type": "string" }, "maxItems": 1 }

maxItems: 1 is how a schema says “there should be exactly one”, which is a check that was not expressible before.

Where a content model does state the cardinality, the key follows it. head.title is a scalar because HTML permits one <title>; DITA’s keys are typed element by element because the OASIS content model says which repeat.

Check one thing before writing a schema against a lifted key. Is it a list or a scalar? It depends on the format, and getting it wrong produces a confusing failure. The field is present and has a value, and the type is simply not the one you declared.

Format Key type Decided by
Generic XML always a list nothing, since XML states no cardinality
HTML scalar for head.title, list otherwise the HTML content model (one <title>)
DITA per key: prolog.author is a list, prolog.source is a scalar the OASIS content model (author* vs source?)
Any elements: config path always a list nothing, since a path states no cardinality
"article.byline": { "type": "string" } // always fails
"article.byline": { "type": "array", "items": { "type": "string" } } // right
"article.byline": { "type": "array", "items": { "type": "string" }, "maxItems": 1 }

The first form fails on every document, including one with exactly one <byline>. Nothing warns you, because it is valid JSON Schema and simply never matches.

Run docmeta get against a real file to see the shape a key actually has before writing a schema for it:

Terminal window
docmeta get article.byline docs/api.xml -f json

elements: reaches what the convention does not

Section titled “elements: reaches what the convention does not”

The convention stops short on purpose. Naming a path is how a repo says “this one, specifically”.

docmeta.config.yaml
elements:
- article/byline/author # → byline.author
- html/head/link@href # → head.link
overrides:
- files: "specs/**"
elements:
- spec/revision # → spec.revision

A path is slash-separated and absolute from the document root. It is a deliberate subset of XPath’s child axis, so predicates and axes are not coming. @attr selects an attribute instead of the element’s text, which is the only way to reach a void element.

The key is still derived by the rule and is never spelled in config. Two ways to name one key is the ambiguity this design removes.

Four behaviours worth knowing:

  • Config extends the convention. An elements: path whose key the convention or a content model already filled has no effect. That is what stops naming concept/prolog/source from retyping it out of the scalar its content model says it is. A path cannot be used to override a key, only to add one the convention does not reach.
  • elements: accumulates where schemas: replaces. A schema set is a complete statement about how a file is judged. Element paths are a list of extra places to look, so the repo-wide list and every matching override all contribute.
  • A named path lifts an empty element, as [""]. The convention skips one; someone who names it wants “present but empty” checked.
  • Paths are validated when the config loads. A typo left to extraction would match nothing and produce no key. The check it was meant to run would silently never happen.

fill writes element metadata back where it was read from. That is not a nicety. If a value were written anywhere else, the field would stay invalid. The next run would propose it again, and CI would never go green.

Replacing the text between an element’s tags, or the value inside an attribute’s quotes, cannot change whether the document is valid. It changes content, not shape. So it works in any dialect, including ones docmeta knows nothing about.

Both spellings work:

Terminal window
docmeta get article.title docs/api.xml
docmeta get /article.title docs/api.xml

Dot-notation tries nested traversal first, which is right for a frontmatter object like author.name. It falls back to the literal key when that misses.