Choosing the topic
The task of the exam project was to pick a single Wikipedia article and lift its content into a Linked Open Data workflow. Wikipedia is uneven terrain for this: some pages are dense with well-catalogued entities (historical figures, works of art) and some are thin (recent events, niche subcultures). A good candidate needs enough named entities to reach the ten-entity threshold set in the guidelines, but it also needs a story worth telling — an angle where the exercise of formalising the article's content into RDF exposes something that plain prose does not.
The 2018 God of War Wikipedia article is such a candidate for two reasons. First, it is populated with well-attested entities: real people involved in production (Cory Barlog as director, Bear McCreary as composer), organisations (Santa Monica Studio, Sony Interactive Entertainment), the hardware platform (PlayStation 4), fictional protagonists (Kratos and Atreus), and a large cast of figures drawn from Norse mythology — all of whom have Wikidata entries, and the human production credits also carry VIAF identifiers. Second, and more interestingly, the article's central narrative device is the game's transition from Greek to Norse mythology and its subsequent reinterpretation of the source myths — a semantic move that turns out to be modellable but not with any single standard property. That gap is where the interesting work of this project happens.
Identifying the entities
Fifteen entities were selected across three layers of narrative distance from the game, plus two subject concepts. Entities were chosen on the criterion that (a) they are explicitly mentioned in the selected portion of the Wikipedia text, and (b) they carry a stable identifier in Wikidata that can serve as an anchor for reconciliation.
Production layer (5)
These are the real-world people and organisations that made the game, together with the hardware platform on which it was released.
| Entity | Type | Role | Wikidata |
|---|---|---|---|
| Cory Barlog | Person | Director | Q5173545 |
| Bear McCreary | Person | Composer | Q621437 |
| Santa Monica Studio | Organisation | Developer | Q656710 |
| Sony Interactive Entertainment | Organisation | Publisher | Q18594 |
| PlayStation 4 | Object | Platform | Q5014725 |
Fictional layer (2)
These are the two protagonists — the entities the article treats as protagonists of the game's narrative. Wikidata's entries for both refer to the game characters themselves.
| Entity | Type | Wikidata |
|---|---|---|
| Kratos | Fictional character | Q2291154 |
| Atreus | Fictional character | Q54896805 |
Mythological layer (6)
These are the six Norse figures the game features. Wikidata's entries for these refer to the mythological figures as attested in the source tradition — not to their in-game portrayals. This distinction becomes central to the modelling choices in Phase 2.
| Entity | Type | Wikidata |
|---|---|---|
| Odin | Mythological figure | Q43610 |
| Thor | Mythological figure | Q42952 |
| Freyja | Mythological figure | Q1647325 |
| Baldur | Mythological figure | Q131658 |
| Loki | Mythological figure | Q133147 |
| Jörmungandr | Mythological figure | Q181227 |
Concepts (2)
Two subject concepts appear across the article and organise the mythological figures under coherent schemes.
| Concept | Wikidata |
|---|---|
| Norse mythology | Q128285 |
| Greek mythology | Q34726 |
Entities considered and discarded
Three entities were considered but dropped or deliberately kept out of the formal set during the encoding pass, and the reasoning is worth recording because it says something about how entity selection should relate to actual textual anchors and to genuine conceptual overlap, not just a shared name.
Runes were included in the first entity list as a recurring concept of the game — they appear as etchings on Kratos's Leviathan Axe and as symbols throughout the visual design. But when the Wikipedia text was selected for TEI encoding, none of the chosen sections mentioned runes explicitly. Keeping the entity would have meant defining it in the taxonomy without any inline ref pointing to a text mention, which weakens the entity's status: an entity without a textual anchor is data-warehouse padding, not something the article actually organises. Runes were removed from the entity set on this ground.
Greek mythology was not in the initial set — the project's angle was Norse — but appears four times in the selected text (Introduction, Setting, and Development twice). It also carries interpretive weight: the game's whole narrative pivots on the transition from Greek to Norse mythology, and Kratos himself is described in the text as "the former Greek God of War". Modelling both mythologies as siblings under skos:Concept and both as dcterms:subject of the game produces a cleaner symmetric account than modelling only one. Greek mythology was added to replace Runes.
Κράτος (Cratos), the Greek personification of strength, was deliberately not added as an entity, despite being discussed on the home page's character gallery as the source of the protagonist's name. The gallery text there is an etymological and thematic observation — the name is an ironic echo, since the game's Kratos becomes the inversion of a minor deity defined entirely by service to someone else's will — not a claim that the game meaningfully reworks that deity's myth the way it reworks Baldur's or Freyja's. Unlike the six Norse figures, where the game redraws specific mythological relationships (genealogy, fate, role), Cratos-the-deity and Kratos-the-character share only the name and a loose thematic contrast; there is no genealogy, event, or trait from the Aeschylus source that the game revises or reuses. Formalising that as a gow:reinterprets triple would overstate a connection that the game itself never develops beyond the name.
Theoretical model
The theoretical model captures, at a domain-agnostic level, the entities and relations that the encoded text needs to represent. It is expressed as a mind map produced in Miro, organised in three colour-coded layers (purple for production, green for concepts, pink/blue for fictional and mythological figures). Arrows are labelled with the natural-language relation they carry — "is directed by", "is developed by", "has fictional character", "reinterprets the mythological figure", "is father of", and so on. The mythological figures are grouped inside a dashed sub-container within the outer Norse mythology concept scheme so that the six reinterprets arrows do not fan out individually; a single arrow ("reinterprets the mythological figures") points at the sub-container, and the interior structure carries the genealogical relations that survive to the RDF phase — including the game's altered Freyja/Baldur motherhood and the Atreus-is-Loki reveal, drawn as a single long arc labelled "reinterprets the mythological figure" connecting Atreus directly to Loki.
TEI/XML encoding
The article was encoded in TEI/XML with a separation between metadata, entity definitions, and running text. The teiHeader declares the source bibliography (the Wikipedia permalink, the consultation date, and the responsible parties) and, inside encodingDesc/classDecl, a taxonomy that carries the two subject concepts (Norse mythology and Greek mythology) with their Wikidata URIs expressed via @corresp attributes.
Individual entities live in a <standOff> block. Each real person and organisation carries an <idno type="Wikidata"> and, where applicable, an <idno type="VIAF">. Fictional characters carry only Wikidata (VIAF does not describe fictional entities). Mythological figures likewise carry only Wikidata, but semantically these identifiers point to the myth version, not the game version — a distinction that is honoured in Phase 3 by using a different reconciliation property. The taxonomy is used for the two concepts because <category> in TEI does not accept <idno> as a child element; @corresp on the category itself is the sanctioned workaround.
Two small encoding traps worth noting
The TEI namespace URI is http://www.tei-c.org/ns/1.0, not http://tei-c.org — using the latter is the single most common newcomer mistake and produces silently invalid files that no downstream tool will parse as TEI. Second, the & in the Wikipedia permalink (?title=…&oldid=…) must be escaped as & inside XML attribute values; a raw ampersand is a syntax error.
Inline annotation in the body follows a uniform pattern: every mention of one of the 15 entities is wrapped in the appropriate TEI element (persName, orgName, objectName, or rs type="concept") with an @ref attribute pointing to the local xml:id defined in the standoff. This gives the graph a clean two-step: from an inline mention, follow @ref to the standoff entry, then follow idno or @corresp to the external authority.
Textual phenomena
Beyond entities, the guidelines require the encoding of textual phenomena — quotations and citations. Direct speech attributed to Cory Barlog and Bear McCreary is wrapped in <said> with a @who attribute pointing to the speaker's standoff xml:id. Unattributed quoted phrases remain encoded as <quote>. This follows the TEI distinction between speech assigned to a speaker and other quoted material, while allowing the HTML rendering to distinguish attributed speech visually. Wikipedia footnote markers ("[26]", "[10]") are preserved as <ref type="citation"> and rendered as small superscript text.
HTML transformation
Both a Python script and an XSLT stylesheet were viable options for the TEI→HTML step; XSLT is arguably the more idiomatic choice for TEI specifically, since it is designed to transform XML tree structures declaratively and many TEI toolchains ship XSLT stylesheets out of the box. Python with lxml was chosen instead for two practical reasons: it keeps the whole pipeline (TEI parsing, HTML generation, and the later RDF serialisation) in a single language the author is already fluent in, which matters when the transformation logic needs to be extended or debugged under time pressure; and it makes it straightforward to share code between the two transformation scripts — both tei_to_html.py and tei_to_rdf.py use the same entity-extraction function shape, so a change to how entities are read from the standoff only has to be reasoned about once, even though the two scripts remain separate files. The trade-off accepted is that the script re-implements, in Python, some of the tree-walking that XSLT would have given for free through template matching.
The TEI file is transformed into a self-contained HTML document by a Python script (tei_to_html.py) that uses lxml for parsing. A sticky sidebar carries the table of contents and a legend of the entity colour scheme. The transformation runs in four steps, each shown below with the actual code and the reasoning behind it. The output of this exact script, running against the current tei.xml, is embedded live on the HTML Rendering page — not just linked as a download.
Step 1 — Extract entities from the standoff
Before any text can be rendered, the script needs a lookup table: given an xml:id like baldur, what is this entity's display name, its role, and its authority identifiers? This is built once by walking <listPerson>, <listOrg>, <listObject>, and the taxonomy's <category> elements, and collecting the result into a plain Python dictionary keyed by xml:id.
def extract_entities(root) -> dict:
entities = {}
for person in root.iter(_q("person")):
pid = _xml_id(person)
if not pid:
continue
name_el = person.find(_q("persName"))
wikidata = viaf = ""
for idno in person.findall(_q("idno")):
if idno.get("type") == "Wikidata":
wikidata = clean_id(idno.text)
elif idno.get("type") == "VIAF":
viaf = clean_id(idno.text)
entities[pid] = {
"type": "person",
"name": name_el.text if name_el is not None else pid,
"role": person.get("role", ""),
"wikidata": wikidata,
"viaf": viaf,
}
# … same pattern repeats for <org>, <object>, <category>
# (concepts get skos-style fields instead of role/viaf)
return entities
This function is deliberately identical in shape to the one used in tei_to_rdf.py (see the Knowledge Representation page): both scripts need the same lookup table, just to feed different output formats downstream. Keeping the extraction logic in one recognisable shape across both scripts means a correction to how, say, VIAF identifiers are cleaned only has to be made — and re-verified — once per script, with the same mental model applying to both.
Step 2 — Build the hover tooltip text
Each entity's tooltip needs to combine whatever fields are actually present — not every entity has a role, not every entity has a VIAF — without producing an awkward string with empty gaps. A small helper function assembles the tooltip conditionally:
def tooltip_for(entity: dict) -> str:
parts = [entity["name"]]
if entity["role"]:
parts.append(f"({entity['role'].replace('-', ' ')})")
tail = []
if entity["wikidata"]:
tail.append(f"Wikidata: {entity['wikidata']}")
if entity["viaf"]:
tail.append(f"VIAF: {viaf_display(entity['viaf'])}")
if tail:
return f"{' '.join(parts)} — {' · '.join(tail)}"
return " ".join(parts)
For example, hovering over Baldur produces "Baldur (mythological figure) — Wikidata: Q131658" — no VIAF clause, since mythological figures don't carry one — while hovering over Cory Barlog produces both clauses. This is why the function branches on presence rather than always concatenating four fixed fields: a fixed-format string would print empty labels for entities missing a field.
Step 3 — Walk the TEI tree and dispatch by tag
The core of the transformation is a recursive function that walks every element in the TEI <body> and decides what HTML to emit based on the element's tag name. Structural elements (div, head, p) become their HTML equivalents; entity elements (persName, orgName, rs) are looked up in the dictionary built in Step 1 and wrapped in a coloured, tooltipped span or link:
def render_element(el, entities: dict) -> str:
tag = etree.QName(el).localname
if tag == "persName":
ref = el.get("ref", "").lstrip("#")
return render_entity_span("person", render_children(el, entities), ref, entities)
if tag == "said":
who = el.get("who", "").lstrip("#")
inner = render_children(el, entities)
if who and who in entities:
speaker = entities[who]["name"]
return (f'<span class="quote attributed" title="Attributed to {speaker}">'
f'“{inner}”</span>')
return f'<span class="quote">“{inner}”</span>'
if tag == "quote":
return f'<span class="quote">“{render_children(el, entities)}”</span>'
# … one branch per TEI tag the encoding uses (orgName, objectName,
# rs, placeName, title, ref, hi); unrecognised tags fall through
# to rendering just their children, so the transformation never
# crashes on an unexpected element — it degrades to plain text.
The separate said and quote branches show why a dispatch-by-tag design pays off. Only said needs to read @who and consult the standoff to identify the speaker, so it receives the frost-bordered attributed style and an attribution tooltip. A plain quote is rendered with quotation marks and the non-attributed style. The visual distinction therefore follows the semantic distinction already encoded in the TEI.
Step 4 — Assemble the page and write it out
The last step collects the rendered body, the table of contents (built by scanning top-level <div> elements for their <head> text), and the document metadata (title, source URL, consultation date, all read straight from teiHeader) into the HTML template, and writes the result to disk:
toc = extract_toc(body)
body_html = render_children(body, entities)
html = HTML_TEMPLATE.format(
page_title=escape(page_title),
source_url=escape(source_url),
toc_items=toc_items,
body=body_html,
css=CSS,
)
Path(output_path).write_text(html, encoding="utf-8")
Nothing about the title, the source link, or the table of contents is hand-typed into the HTML template — all three are derived from the TEI file at run time, which is what makes the transformation reproducible: editing tei.xml and re-running the script regenerates a consistent page without anyone having to remember to also update a separately maintained HTML file.
The five entity types have distinct colour treatments so the density and distribution of entities across sections is visible at a glance:
- Person — real people, fictional characters, and mythological figures
- Organisation — Santa Monica Studio, Sony Interactive Entertainment
- Object — PlayStation 4
- Concept — Norse mythology, Greek mythology
- Mythical place — Midgard, Asgard, Jötunheim, and the other six realms; not modelled as first-class entities but marked in the text
The Resources page links to the raw TEI file, the transformation script, and the generated HTML.