From 89de8ff5c95149d79120f4062fcf5d1e0a82e641 Mon Sep 17 00:00:00 2001
From: Jacobus Geluk
Date: Fri, 21 Aug 2026 13:58:26 +0100
Subject: [PATCH] fix(spec): stop the spec header advertising URLs that 404
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Checking all 103 links in the rendered spec found four distinct causes, three
of them in the header ReSpec builds client-side, where none of it is visible
without loading the page in a browser.
- edDraftURI was https://ekgf.org/spec/{branch}/ — missing the /dprod
basePath, and with the branch name unslugified, so any branch containing a
slash could never resolve. It now points at the one canonical draft,
/dprod/spec/develop/, so a preview reader is sent to mainline.
- The per-branch URL moves to thisVersion, which is the field that means it,
and uses the slug the site's own routing uses.
- latestVersion was the vocabulary namespace IRI, which identifies the DPROD
terms rather than a document and does not dereference. It is now the OMG
catalog entry, which always resolves to the newest published version.
- The frozen 1.0 archive set no edDraftURI at all, so ReSpec inferred one from
the github config: the GitHub Pages site retired by #235. Every reader of the
published standard met a 404 that appears nowhere in the file. Its
latestVersion also pointed at itself, which is what thisVersion means.
Two smaller defects, both in the template and the archive: an href that
swallowed the sentence's full stop (tm_list.htm.), and prose links to an
absolute dprod.ttl that 404s where the "Download as RDF" entries beside them
already use a relative path that resolves per version.
The namespace IRI is now rendered as code rather than as a link, matching the
namespace table further down, and the [[dprod]] bibliography entry cites the
OMG catalog.
tests/test_spec_header_urls.py locks all of it in. Writing it immediately
caught the trailing full stop in the archive, which the manual pass missed.
Not fixable here: the DPROD namespace IRI and dprod.ttl do not resolve at OMG
at all, the same gap as #232. This routes around them.
Resolves: #255
---
respec/template.html | 26 ++++--
site/public/spec/archive/1.0/index.html | 12 ++-
spec-generator/main.py | 11 +++
tests/test_spec_header_urls.py | 107 ++++++++++++++++++++++++
4 files changed, 147 insertions(+), 9 deletions(-)
create mode 100644 tests/test_spec_header_urls.py
diff --git a/respec/template.html b/respec/template.html
index e347c87..a8f3124 100644
--- a/respec/template.html
+++ b/respec/template.html
@@ -11,8 +11,17 @@
const respecConfig = {
// Working Groups ids at https://respec.org/w3c/groups/
// group: "Semantic Data Products Working Group",
- latestVersion: "https://www.omg.org/spec/DPROD/dprod/",
- edDraftURI: "https://ekgf.org/spec/{{ branch }}/",
+ // The OMG catalog entry, which always resolves to the newest published
+ // version. Not the ontology namespace IRI (.../DPROD/dprod/): that
+ // identifies the vocabulary, not a document, and 404s (issue #255).
+ latestVersion: "https://www.omg.org/spec/DPROD/",
+ // The one canonical continuously-updated draft, whichever branch this
+ // build came from. A branch preview points readers at mainline; its own
+ // URL is thisVersion below.
+ edDraftURI: "https://ekgf.org/dprod/spec/develop/",
+ // This build. The slug is the branch with slashes replaced by dashes,
+ // and the /dprod basePath is required — without it the URL 404s.
+ thisVersion: "https://ekgf.org/dprod/spec/{{ branch_slug }}/",
specStatus: "base",
// Pinned to the branch's last commit by the spec generator. Without it
// ReSpec uses the date the page is viewed, since it runs in the reader's
@@ -148,7 +157,10 @@
},
dprod: {
title: "Data Product Ontology",
- href: "https://www.omg.org/spec/DPROD/dprod/",
+ // The OMG catalog entry, which resolves. The vocabulary namespace
+ // IRI (.../DPROD/dprod/) identifies the terms rather than a
+ // document, and does not currently dereference (issue #255).
+ href: "https://www.omg.org/spec/DPROD/",
authors: [
"Tony Seale",
"Natasa Varytimou",
@@ -393,7 +405,7 @@
For a complete list of trademarks,
- see: https://www.omg.org/legal/tm_list.htm.
+ see: https://www.omg.org/legal/tm_list.htm.
All other products or company names mentioned are used for identification purposes only and may be
trademarks of their respective owners.
@@ -457,7 +469,7 @@ Data Product Ontology (DPROD)
Scope
- The Data Product (DPROD) specification is a
+ The Data Product (DPROD) specification is a
profile of the Data Catalog (DCAT) Vocabulary,
designed to describe Data Products.
This document defines the schema and provides examples of its use.
@@ -470,7 +482,7 @@
Scope
and enables federated search across multiple sites using a uniform query mechanism and structure.
- The namespace for DPROD terms is https://www.omg.org/spec/DPROD/dprod/
+ The namespace for DPROD terms is https://www.omg.org/spec/DPROD/dprod/
The suggested prefix for the DPROD namespace is dprod
@@ -484,7 +496,7 @@
Scope
Harmonize Data Schemas: Using shared schemas helps unify different data formats.
- For instance, the DPROD specification provides a
+ For instance, the DPROD specification provides a
common set of rules for defining a Data Product.
Users of DPROD can extend this schema as needed.
diff --git a/site/public/spec/archive/1.0/index.html b/site/public/spec/archive/1.0/index.html
index bf7ba89..b13d178 100644
--- a/site/public/spec/archive/1.0/index.html
+++ b/site/public/spec/archive/1.0/index.html
@@ -11,7 +11,15 @@
const respecConfig = {
// Working Groups ids at https://respec.org/w3c/groups/
// group: "Semantic Data Products Working Group",
- latestVersion: "https://ekgf.org/dprod/spec/main/",
+ // The OMG catalog entry, so a reader of this frozen 1.0 can reach the
+ // newest published version. It previously pointed at this document
+ // itself, which is what "this version" means, not "latest".
+ latestVersion: "https://www.omg.org/spec/DPROD/",
+ // Set explicitly. When edDraftURI is absent ReSpec infers it from the
+ // github config as https://ekgf.github.io/dprod/ — GitHub Pages, retired
+ // by #235, so every reader of the frozen standard met a 404 (issue #255).
+ edDraftURI: "https://ekgf.org/dprod/spec/develop/",
+ thisVersion: "https://ekgf.org/dprod/spec/main/",
specStatus: "base",
// Frozen: DPROD 1.0 beta 1. Hard-coded rather than derived, because this
// document is immutable and its date must not move when a maintenance
@@ -389,7 +397,7 @@
For a complete list of trademarks,
- see: https://www.omg.org/legal/tm_list.htm.
+ see: https://www.omg.org/legal/tm_list.htm.
All other products or company names mentioned are used for identification purposes only and may be
trademarks of their respective owners.
diff --git a/spec-generator/main.py b/spec-generator/main.py
index abd2993..3fe7800 100644
--- a/spec-generator/main.py
+++ b/spec-generator/main.py
@@ -30,6 +30,16 @@ def detect_branch() -> str:
return "main"
+def branch_slug(branch: str) -> str:
+ """The branch name as it appears in a /spec// URL.
+
+ Slashes become dashes, matching site/src/lib/spec-versions.ts. The raw
+ branch name is still what ReSpec's `github` config needs for its commit
+ history link, so both are passed to the template.
+ """
+ return branch.replace("/", "-")
+
+
def detect_publish_date(g) -> str:
"""The publication date of this version, from `dct:issued` on the ontology.
@@ -127,6 +137,7 @@ def main():
'classes': classes,
'examples': examples,
'branch': detect_branch(),
+ 'branch_slug': branch_slug(detect_branch()),
'publish_date': detect_publish_date(g),
})
diff --git a/tests/test_spec_header_urls.py b/tests/test_spec_header_urls.py
new file mode 100644
index 0000000..3bc84a0
--- /dev/null
+++ b/tests/test_spec_header_urls.py
@@ -0,0 +1,107 @@
+"""The spec header must not advertise URLs that 404.
+
+Three separate dead links shipped at once (issue #255):
+
+* `edDraftURI` pointed at `https://ekgf.org/spec/{branch}/` — missing the
+ `/dprod` basePath, and with the branch name unslugified, so a branch
+ containing a slash could never resolve;
+* `latestVersion` pointed at the *vocabulary namespace* IRI, which identifies
+ the terms rather than a document and does not dereference;
+* the frozen 1.0 archive set no `edDraftURI` at all, so ReSpec inferred one
+ from the `github` config — the GitHub Pages site retired by #235.
+
+These are cheap to reintroduce and invisible without rendering the page in a
+browser, since ReSpec builds the header client-side.
+"""
+
+from __future__ import annotations
+
+import re
+import unittest
+from pathlib import Path
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
+SPEC_TEMPLATE = REPOSITORY_ROOT / "respec" / "template.html"
+ARCHIVE_SPEC = (
+ REPOSITORY_ROOT / "site" / "public" / "spec" / "archive" / "1.0" / "index.html"
+)
+
+#: Hosts and paths known not to resolve.
+RETIRED_URLS = (
+ "ekgf.github.io/dprod",
+ "https://www.omg.org/spec/DPROD/dprod.ttl",
+)
+SPEC_BASE = "https://ekgf.org/dprod/spec/"
+
+
+class SpecHeaderUrlTest(unittest.TestCase):
+ @staticmethod
+ def without_comments(content: str) -> str:
+ """Strip comments: a URL explained in one is not a URL that is linked."""
+ content = re.sub(r"", "", content, flags=re.DOTALL)
+ return "\n".join(
+ line for line in content.splitlines() if not line.lstrip().startswith("//")
+ )
+
+ def documents(self) -> list[tuple[str, str]]:
+ return [
+ (
+ path.relative_to(REPOSITORY_ROOT).as_posix(),
+ self.without_comments(path.read_text("utf-8")),
+ )
+ for path in (SPEC_TEMPLATE, ARCHIVE_SPEC)
+ ]
+
+ def test_no_retired_urls_are_linked(self) -> None:
+ for name, content in self.documents():
+ for retired in RETIRED_URLS:
+ with self.subTest(document=name, url=retired):
+ self.assertNotIn(
+ retired,
+ content,
+ f"{name} references {retired}, which does not resolve.",
+ )
+
+ def test_both_documents_set_an_editors_draft(self) -> None:
+ """Unset, ReSpec infers the retired GitHub Pages URL from `github`."""
+ for name, content in self.documents():
+ with self.subTest(document=name):
+ self.assertIn(
+ f'edDraftURI: "{SPEC_BASE}develop/"',
+ content,
+ f"{name} must set edDraftURI explicitly to the canonical "
+ f"develop draft.",
+ )
+
+ def test_latest_version_is_the_omg_catalog(self) -> None:
+ for name, content in self.documents():
+ with self.subTest(document=name):
+ self.assertIn(
+ 'latestVersion: "https://www.omg.org/spec/DPROD/"',
+ content,
+ f"{name}: latestVersion must be the OMG catalog entry, which "
+ f"always resolves to the newest published version.",
+ )
+
+ def test_this_version_carries_the_basepath_and_a_slug(self) -> None:
+ template = SPEC_TEMPLATE.read_text(encoding="utf-8")
+ self.assertIn(
+ f'thisVersion: "{SPEC_BASE}{{{{ branch_slug }}}}/"',
+ template,
+ "thisVersion must use the /dprod basePath and the slugified branch; "
+ "a raw branch name containing a slash does not resolve.",
+ )
+
+ archive = ARCHIVE_SPEC.read_text(encoding="utf-8")
+ self.assertIn(f'thisVersion: "{SPEC_BASE}main/"', archive)
+
+ def test_no_href_swallows_a_trailing_full_stop(self) -> None:
+ """`href="....htm."` — the sentence's full stop inside the URL."""
+ for name, content in self.documents():
+ with self.subTest(document=name):
+ offenders = re.findall(r'href="[^"]*\.(?:htm|html|org|com)\."', content)
+ self.assertEqual([], offenders, f"{name}: {offenders}")
+
+
+if __name__ == "__main__":
+ unittest.main()