DITA Map Design: Using Keys to Produce Reliable Anchor IDs for Deliverables

When you generate online deliverables from DITA content the users of those deliverables may need to create links to things in your deliverables. For example, they may want to create links to the HTML page for a specific topic or link to a named destination within a PDF that is served online. They will do this by creating links to specific HTML pages, @id values within HTML pages, PDF named destinations, or whatever form of "anchor" the online deliverable provides.

When they do this, they have a reasonable expectation that their links will continue to work as the deliverables are revised and republished.

Thus users of DITA-based deliverables need to have a reliable set of "public" anchor IDs that they can link to: HTML files, @id values on HTML elements, PDF named destinations, help topic identifiers, etc.

Unfortunately, producing these public anchor IDs can be a challenge.

This paper describes a practice for using DITA keys that allows for the reliable generation of public anchor IDs.

XML IDs and Topic Filenames Won't Work

In traditional XML practice, where one publication is represented by exactly one XML document, XML IDs were sufficient to generate reliable and persistent anchors in any deliverable, because XML IDs must be unique within the scope of the XML document that contains them. This is a simplification but a powerful simplification because it avoids lots of complexity.

Unfortunately, this simplification cannot be used with DITA when using DITA in the normal way of maps and topics, especially when there is re-use.

There are a number of reasons a simple mapping of source filenames and IDs to deliverable anchor IDs will not work:

  • There is no necessary relationship between the DITA source filenames or IDs and any deliverable artifact (HTML file, PDF named destination, etc.). In the context of a component content management system topics may not be files at all and therefore may not have any kind of useful identifier other than an object ID, which may not be suitable for use as a public ID (for example, because it's an opaque number or a long GUID or whatever).
  • Purely generated IDs are arbitrary and not predictable. They may be truly arbitrary or may reflect details of the XML files or file organization, which can change from release to release. So generating IDs for anchors in the output, as is done by the Open Toolkit's PDF2 transform as of OT 1.8, is not useful.
  • Topic IDs in DITA need not be unique outside the scope of the XML document that contains the topic. If topic IDs are not unique at least within the scope of a single publication, they obviously can't be used for public anchor IDs.
  • Element IDs for non-topic elements within topics are only unique within the scope of their direct-parent topic. If you have nested topics within a single XML document you could easily have multiple elements with the ID "step-01" or whatever. (However, within the same XML document, any topic ID/element ID pair will be unique, because topic IDs are true XML IDs and thus must be unique within the same XML document.
  • Element IDs for elements within maps are only unique within the XML document that contains the element, so maps combined into a map tree could use the same IDs for elements in those maps, meaning you could have topicrefs with the ID "installation" in two different maps, both of which are used from some parent map.

This all means that element IDs (@id values) and topic filenames cannot be relied on as the basis for producing public anchor IDs.

While one can imagine imposing policies and processing on your DITA source to make it be the case that topic IDs are globally unique or whatever, it's ultimately a futile effort, because you may need to integrate new content in the future where the IDs are not controlled, meaning you either have to modify that content to rationalize the IDs or modify your existing content to rationalize the IDs, neither of which are attractive options. In any case, such effort is unnecessary because DITA, starting with version 1.2, provides facilities that can solve the public anchor ID problem without unnecessary effort.

Using Keys to Enable Public Anchor IDs

The DITA key facility, introduced in DITA 1.2, provides a way to associate arbitrary names (keys) with topics, maps, and non-DITA resources so that you can refer to things by key rather than by direct URI reference.

In particular, you can refer to topics (and, by extension, elements within topics) by key.

Keys are a form of indirect address, meaning that references are to keys and then the keys are bound to their ultimate references. Indirection is essential for authoring because it protects the documents making the references from changes in the details of where the ultimate targets are.

For example, if you create a cross reference to a topic by direct URL reference using the @href attribute of <xref>, then if that topic is moved to a different location or it's source file is renamed, your link breaks. Likewise, if you use this topic in a different map you must also use the topic you've linked to or the link will be broken in the result.

Keys are defined in maps, which means that two different maps can bind the same key name to different resources. This is essential for re-use because it allows a topic to be used in the context of different maps and have a link from that topic resolve to different targets in the context of different maps. For example, you might have a common topic applicable to multiple products that needs to link to product-specific installation instructions. If you have different maps you can have the same key (e.g. "installation") bound to different product-specific topics.

In DITA 1.2 key names are global to the root map they're defined within.

This means that keys do represent a reliable set of globally-unique names within the scope of a given publication (assuming that one root map produces one deliverable, which is the normal case).

In addition, keys are entirely under the control of map authors, so they can be as persistent and meaningful as desired. Beyond that, one key can refer to another key, which means you can use keys as aliases for other keys, allowing you to maintain old IDs that aren't directly associated with topics but that somebody might still have links to.

However, it's not quite as simple as associating a key with every topic.

Resource and Navigation Topicrefs

Starting with DITA 1.2, DITA defines two kinds of topicrefs: normal and "resource only".

Resource-only topicrefs are topicrefs that simply establish a dependency on a thing (a topic, an image, etc.) but do not define where that thing might be used in the publication's navigation structure. This "processing role" is defined by the @processing-role attribute on topicrefs. A value of "resource-only" indicates a resource-only topicref. A value of "normal" indicates a normal topicref. The <keydef> element is a specialization of <topicref> that sets the default value of @processing-role to "resource-only".

When used outside of relationship tables, normal-role topicrefs define the navigation structure of the publication, meaning that whatever they point to is expected to be reflected in the resulting deliverable. E.g., the navigation topicrefs define the "table of contents" for the publication. I refer to these topicrefs as "navigation" topicrefs, to distinguish them from both resource-only topicrefs and normal-role topicrefs used in relationship tables (which establish links among topics but do not define a ToC-style navigation structure).

This distinction between resource-only topicrefs and navigation topicrefs is very important.

Resource-only topicrefs establish a dependency from the map to topics, including topics that may never be used directly in the output but only serve as a source of elements to be used by reference via conref links.

Navigation topicrefs represent unique uses of topics. When a topic is used multiple times within a map, each topicref that references that topic establishes a different use of that topic, at a unique place within the publication's navigation structure. This means that by addressing the topicref to a topic you can address a specific use of the topic.

This ability to address specific uses becomes important when you have cross references from one topic to another topic, where the target topic is used multiple times in the map. In that case, which use of the topic do you want the link to go to? Without a way to address specific uses there's no DITA-defined way to say.

Always Use Keys

Just to make this whole discussion clear: You should use keys for all references: conrefs, xrefs, topicrefs from navigation topicrefs, references to images, references to external non-DITA resources, everything.

This discussion presumes that you're doing this. The whole point of this discussion is to describe how you should be doing it.

Resource Only and Navigation Keys

You can associate keys with both resource-only and navigation topicrefs.

However, there are important differences between resource-only keys and navigation keys and those differences have important implications for public anchor IDs.

Resource-only keys make is possible to make key references to topics for any purpose, but in particular, for content reference. Thus, any topic that might be the target of a content reference link must have a resource-only key defined for it. As this could be any topic, it follows that as a matter of practice you should define resource-only keys for every topic used from a map.

However, these resource-only keys are not useful as the base for public anchor IDs because they are not bound to any particular navigation point in the publication, by definition. There is also no guarantee that a given topic with a resource-only key will even be directly reflected in the result—it might only ever be used for conref.

The only keys that can be reliably used as the base for public anchor IDs are keys on navigation topicrefs. Because navigation topicrefs represent unique uses of topics, they are reliable as persistent anchors for specific uses of topics, which is what you need to enable reliable addressing.

Thus you should also put keys on any navigation topicref that you want to allow users of your deliverable to link to. This might be all navigation topicrefs or it might only be specific ones, depending on what your public linking policy is or the nature of your content or whatever. The simplest practice is to simply put a key on every navigation topicref.

Because key names are global within a given root map, you cannot use the same key name on a resource-only topicref and a navigation topicref: only one will be effective because of the precedence rules for keys.

Thus, for each topic you need at least two keys: one for the resource-only topicref to the topic and one for the each navigation topicref to the topic.

Note that since you already have resource-only keys for each topic, your navigation topicrefs should themselves use keyref to point to the topic to use:
<map>
  <title>My Publication</title>
  <topicgroup><!-- Resource-only topicrefs -->
    <keydef keys="topic-01"
            href="topics/topic-00001.dita"
    />
    <keydef keys="topic-02"
            href="topics/topic-00002.dita"
    />
    ...
  </topicgroup>
  <topicgroup><!-- Navigation structure -->
     <topicref keys="chap-01"
        keyref="topic-01"
     />
     <topicref keys="chap-02"
        keyref="topic-02"
     />
     ...
  </topicgroup>
</map>

Note the key names used in this example. The resource-only keys ("topic-01", "topic-02") are arbitrary identifiers, useful for simply pointing to the topics but not reflective of anything about the topic. By contrast, the navigation keys ("chap-01", "chap-02") reflect the publication structure and are meaningful to a human looking at the IDs.

This suggests that you should have a clear and consistent naming convention for these two types of key so that it's clear to authors which key they're referring to so that they choose the correct one for the correct purpose (resource-only key for conrefs, navigation key for cross references).

This use of resource-only topicrefs with navigation topicrefs that refer to the resource-only keys means that there only needs to be one topicref that points directly to the topic file by direct URI—all other references are by key. This means that if the location of the topic changes you only have to update one place to react to that change. The key-based references will be unaffected.

It also means that the same navigation structure can be used in different maps with different topics bound to the same resource-only keys, if that makes sense (it doesn't always).

Note that typical practice is to organize resource-only topicrefs into separate map documents that contain only resource-only key definitions. These maps both make it easier to manage the key definitions, whether manually or through automation, and serve as "catalogs" of topics available for use by authors. You can have different sets of these key-defining maps to organize topics in useful ways, such as all warehouse topics in one set of keys, all topics on a specific subject, or whatever.

Key Definition Practice Summary

To sum up:
  1. Define a resource-only topicref for every topic required by the map (<keydef keys="topic-01" href="topics/topic-00001.dita"/>)
  2. Use key references to the resource-only keys from navigation topicrefs (<topicref keys="chap-01" keyref="topic-01" />)
  3. Put keys on navigation topicrefs that you want to allow external linking to.

Given keys on all appropriate navigation topicrefs, you can then reliably generate persistent anchor IDs for each navigation topicref, meaning each use of each topic actually used in the deliverable's content.

Scoped Keys

DITA 1.3 introduces scoped keys, which allow you to define named key scopes within a map such that references to unqualified keys within a given scope will resolve to the key name defined within that scope, if any.

This allows you to have things like multi-product books where each product has a different installation topic referenced from a generic topic used in each product's scope. If you make each product a key scope then you can have an unqualified key reference, e.g. "keyref='installation'" and have it resolve to different topics within each product's scope.

This has some implications for deliverable ID generation. In particular, each key defined within a scope has an implicit qualified value that reflects the full scope hierarchy from the root map to the key.

For example, if you have the three scopes "prod1", "prod2", and "prod3", defined in your map, and then within each scope define the key "installation", the fully-qualified result is three keys named "prod1.installation", "prod2.installation", and "prod3.installation".

This means that even though the key definitions and references within the same scope are unqualified as authored, they actually represent the same global key space you have in DITA 1.2.

This means that scoped keys can be used to reliably generate public anchor IDs by using the fully-qualfied key names.

Non-Topic Elements

You can address non-topic elements using keys by using key references of the form "keyname/element ID", where elementID is the ID of a non-topic element within a topic.

For example, given this topic:
<topic id="topicid">
  <title>Topic Three</title>
  <body>
    <p>This is a topic.</p>
    <fig id="fig-01">
      <title>A figure</title>
      ...
    </fig>
  </body>
</topic>
If you bind the topic to the key "topic-03" then you can address the figure using the key reference "topic-03/fig-01":
<p>See <xref keyref="topic-03/fig-01"/> for an illustration.</p>

This presents a challenge for the generation of anchor IDs: if you link to something one of your readers may want to link to it to. Thus you really need to generate nice anchor IDs for at least those non-topic elements that you link to in your own content.

But, you might have things what would be useful to link to that you don't link to yourself, such as other figures, tables, key steps within procedures, etc.

However, you can't link to something at all if it doesn't have an ID and generating IDs for those things that might be useful to link to but that don't have IDs is problematic, as discussed at the start of this paper. Thus, you may need to add IDs to those things that should be publicly linkable even if you don't have an immediate need to link to them. Figures, tables, sections, and task steps are obvious candidates for this type of policy.

Likewise, if you have an author or component content management system that adds IDs to lots of things, many of which you would never want to link to or enable linking to, you don't necessarily want to generate anchor IDs for every element that happens to have an ID, although that can be an easy policy to implement.

Generating Deliverable Anchor IDs

The implication of using keys for deliverable ID generation should be clear: each navigation topicref's keys become anchor IDs. However, it's not necessarily that simple.

For this discussion I am using term "navigation key" to mean "a key defined on a navigation topicref"

For the purpose of discussing deliverables we can divide deliverables into two types: multi-file and monolithic.

Multi-file deliverables are things like HTML, where there are multiple result files linked together. HTML is the obvious example, but there are also proprietary help systems, Wiki systems, and so on, that do not use HTML as the base data format. In multi-file deliverables there is a useful notion of "file" and "filename" where path to a file is an essential part of its address. In multi-file deliverables there can be a natural correspondence between the source DITA files and the result deliverable files (but there doesn't have to be any necessary correspondence and in any case such correspondence cannot be guaranteed in the general case).

Monolithic deliverables are things like PDF, where the result is a single file and all anchors are within the context of the single result file.

Multi-file Deliverable Anchor IDs

For multie-file deliverables there are two aspects to ID generation: paths to files and anchor IDs within files.

When the topic to be addressed becomes a file in the result, then the anchor ID is the path to the file in the result.

Given the navigation topicref:
<topicref keys="chap-01"
  keyref="topic-01"
/>
the result HTML file could be named "chap-01.html".

Because the name reflects a key, which must be unique within the publication, we know the filename must be unique for all HTML files whose filenames reflect a key. Likewise, the filename will be persistent in future versions of the deliverable as long as the key "chap-01" is maintained. The deliverable generation could have rules for constructing a directory structure, such as making each level in navigation structure a directory in the output or organizing result files by topic type or making each key scope a new directory. The important thing is that the rule has to be consistent and repeatable, so that the directory structure is consistent over time for consistent map structures.

However, navigation topicrefs do not necessarily always result in separate HTML files.

You can use the @chunk attribute to combine multiple topics into a single result file:
<topicref keys="chap-01"
  keyref="topic-01"
  chunk="to-content select-branch"
>
  <topicref keys="chap-01-ss-01"
    keyref="topic-1234"
  />
  <topicref keys="chap-01-ss-02"
    keyref="topic-1234"
  />
</topicref>

Here the @chunk value will result in a single HTML file with the topics "chap-01-ss-01" and "chap-01-ss-02" as HTML markup within that file.

Thus, for those topics the processor will need to generate HTML anchors or elements with the IDs "chap-01-ss-01" and "chap-01-ss-02".

Because HTML files establish new ID scopes it is sufficient to ensure that generated IDs are unique within a given HTML file. This means, for example, that you could take advantage of key scopes when there is an alignment between key scopes and the HTML file structure such that you can generate IDs reflecting unqualified keys (because the HTML file or directory structure itself is sufficient to reflect the scope qualification).

Monolithic Deliverable Anchor IDs

Monolithic deliverables present the challenge that they typically provide a single name space for anchor IDs. In the case of PDF there is just global named destinations.

For topics, the fully-qualified keys can be used directly as named destinations as they are guaranteed unique and should satisfy the syntax requirements of PDF named destinations.

For elements within topics the topic's key must be combined with the element's ID to form a single destination value. This value should be easy to use within URLs so that it can be used without the need for escaping.

This means that you must choose some way to separate the topic key name from the element ID that will be likely to no occur within keys or IDs (so you don't result in accidental collisions) and that doesn't need to be escaped, such as "._." or ".-." or similar.

Given this convention you can then generate named destinations, or their equivalent, in the PDF, enabling reliable addressing of things within the PDF.

All the major XSL-FO engines provide extensions for creating PDF named destinations.

Given a named destination you can link to it using fragment identifiers of the form "#nameddest={destination name}", e.g.: See <xref href="http://example.com/publications/pub1/pub1.pdf#nameddest=chap-01">Chapter 1</xref>.