ELPX Metadata¶
This document describes the three top-level metadata containers in content.xml (<userPreferences>, <odeResources>, <odeProperties>), every property key they carry, serialization rules, and round-trip behaviour through the importer.
See also: ELPX format overview | Container | IDs | Pages and blocks
Overview¶
The <ode> root element contains three metadata containers before <odeNavStructures>. All three are optional in the DTD (content.dtd:20) but are always emitted by OdeXmlGenerator:
<ode xmlns="http://www.intef.es/xsd/ode" version="2.0">
<userPreferences> ... </userPreferences>
<odeResources> ... </odeResources>
<odeProperties> ... </odeProperties>
<odeNavStructures> ... </odeNavStructures>
</ode>
1. <userPreferences>¶
Stores user-level configuration. Every entry is a <userPreference> with a <key> and <value> child.
The generator (OdeXmlGenerator.ts:77–82) emits exactly one entry:
function generateUserPreferencesXml(meta: ExportMetadata): string {
let xml = '<userPreferences>\n';
xml += generateUserPreferenceEntry('theme', meta.theme || 'base');
xml += '</userPreferences>\n';
return xml;
}
| Key | Default | Description |
|---|---|---|
theme |
'base' |
Active theme name. Matches the theme directory bundled in theme/ inside the ZIP. |
No other keys are currently written to <userPreferences> by the generator. Additional keys may appear in files produced by older versions and should be preserved on round-trip.
<userPreferences>
<userPreference>
<key>theme</key>
<value>base</value>
</userPreference>
</userPreferences>
2. <odeResources>¶
Stores package-level identifiers and the generator version. Every entry is an <odeResource> with a <key> and <value> child.
The generator (OdeXmlGenerator.ts:97–103) emits exactly three entries:
function generateOdeResourcesXml(odeId: string, versionId: string): string {
let xml = '<odeResources>\n';
xml += generateOdeResourceEntry('odeId', odeId);
xml += generateOdeResourceEntry('odeVersionId', versionId);
xml += generateOdeResourceEntry('exe_version', ODE_VERSION);
xml += '</odeResources>\n';
return xml;
}
ODE_VERSION is the constant '3.0' defined in constants.ts:1000.
| Key | Description |
|---|---|
odeId |
Stable project identifier. Retrieved from meta.odeIdentifier on export; generated once via generateOdeId() if absent. |
odeVersionId |
Stable per-version identifier. Read from meta.odeVersionId on export; generated once via generateOdeId() only when absent. Imported from <odeResources> so a round-trip without content changes preserves the value. |
exe_version |
The ODE format version string, currently '3.0' (constant ODE_VERSION from constants.ts:1000). |
Legacy keys observed in older fixtures: the v4 generator writes exactly three resources (odeId, odeVersionId, exe_version). Some older fixtures additionally carry odeVersionName, isDownload, or the misspelling eXeVersion. The importer accepts all of these for backward compatibility, but generateOdeResourcesXml() never emits them in v4 — treat them as informational legacy keys when reading older .elpx files.
<odeResources>
<odeResource>
<key>odeId</key>
<value>20251125215855LURLBW</value>
</odeResource>
<odeResource>
<key>odeVersionId</key>
<value>20251125220103ABCXYZ</value>
</odeResource>
<odeResource>
<key>exe_version</key>
<value>3.0</value>
</odeResource>
</odeResources>
3. <odeProperties>¶
Stores document metadata. Every entry is an <odeProperty> with a <key> and <value> child. The generator iterates Object.entries(meta) and emits one entry per property that is not excluded, not empty, and not null/undefined (OdeXmlGenerator.ts:121–138).
The single source of truth for every property is METADATA_PROPERTIES in metadata-properties.ts.
Full property table¶
| Internal key | XML key | Type | Default | Category | Description |
|---|---|---|---|---|---|
title |
pp_title |
string | 'eXeLearning' |
core | Project title |
subtitle |
pp_subtitle |
string | '' |
core | Project subtitle |
author |
pp_author |
string | '' |
core | Author name |
description |
pp_description |
string | '' |
core | Project description |
language |
pp_lang |
string | 'en' |
core | Language code (BCP 47, e.g. 'en', 'es') |
license |
pp_license |
string | '' |
core | License identifier (e.g. 'creative commons: attribution - share alike 4.0') |
licenseUrl |
pp_licenseUrl |
string | '' |
core | License URL |
keywords |
pp_keywords |
string | '' |
core | Comma-separated keywords |
category |
pp_category |
string | '' |
core | Content category |
theme |
pp_theme |
string | 'base' |
core | Theme name (also written to <userPreferences>) |
customStyles |
pp_customStyles |
string | '' |
core | Custom CSS injected into all pages |
exelearningVersion |
pp_exelearning_version |
string | '' |
core | eXeLearning application version string |
addExeLink |
pp_addExeLink |
boolean | true |
export | Include "Made with eXeLearning" footer link |
addPagination |
pp_addPagination |
boolean | false |
export | Add page navigation arrows |
addSearchBox |
pp_addSearchBox |
boolean | false |
export | Include search box |
addAccessibilityToolbar |
pp_addAccessibilityToolbar |
boolean | false |
export | Include accessibility toolbar |
addMathJax |
pp_addMathJax |
boolean | false |
export | Load MathJax for LaTeX rendering |
exportSource |
exportSource |
boolean | true |
export | Include editable source in export (no pp_ prefix — legacy compatibility) |
globalFont |
pp_globalFont |
string | 'default' |
export | Global font override |
extraHeadContent |
pp_extraHeadContent |
string | '' |
content | Custom HTML injected into <head> of all pages |
footer |
footer |
string | '' |
content | Custom footer HTML (no pp_ prefix — legacy compatibility) |
Properties with excludeFromXml: true are not emitted to <odeProperties>:
| Internal key | Reason excluded |
|---|---|
odeIdentifier |
Written to <odeResources> as odeId; stored internally in Yjs but not in the property section. |
createdAt |
Internal timestamp; not persisted to XML. |
modifiedAt |
Internal timestamp; not persisted to XML. |
scormIdentifier |
Goes into the SCORM manifest (imsmanifest.xml), not content.xml. |
masteryScore |
Goes into the SCORM manifest, not content.xml. |
Boolean serialization¶
Booleans are stored as the literal strings "true" or "false". The valueToXmlString() function (metadata-properties.ts:354–360) handles this:
export function valueToXmlString(key: string, value: unknown): string {
const config = getPropertyConfig(key);
if (config?.type === 'boolean') {
return value === true || value === 'true' ? 'true' : 'false';
}
return String(value ?? '');
}
The XSD (ode-content.xsd:242–249) defines a booleanStringType that accepts "true", "false", "True", and "False". On import, the parser normalises these via value.toLowerCase() === 'true'.
Round-trip: XML key to internal key mapping¶
On import, xml-parser.ts reads each <odeProperty> and maps its <key> text back to the internal property name using getInternalKeyForXmlKey() (metadata-properties.ts:287–290):
export function getInternalKeyForXmlKey(xmlKey: string): string | undefined {
const config = getPropertyConfigByXmlKey(xmlKey);
return config?.key;
}
getPropertyConfigByXmlKey() performs a case-insensitive lookup by lowercasing both the candidate key and the stored xmlKey values:
export function getPropertyConfigByXmlKey(xmlKey: string): MetadataPropertyConfig | undefined {
const lowerXmlKey = xmlKey.toLowerCase();
return METADATA_PROPERTIES.find(p => p.xmlKey.toLowerCase() === lowerXmlKey);
}
This means pp_Title, PP_TITLE, and pp_title all resolve to the internal key title. Keys not found in METADATA_PROPERTIES are currently dropped by the parser (no fallback passthrough for unknown keys).
Legacy .elp files¶
Legacy .elp files use Python pickle XML (contentv3.xml), not ODE 2.0. Metadata is stored as a <dictionary> of Python object fields, not <odeProperty> entries. The LegacyXmlParser.ts handles this format. For full details see Legacy ELP Format (contentv3.xml).
Annotated example: all three sections fully populated¶
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ode SYSTEM "content.dtd">
<ode xmlns="http://www.intef.es/xsd/ode" version="2.0">
<!-- Section 1: user-level configuration -->
<userPreferences>
<userPreference>
<key>theme</key>
<value>base</value> <!-- only key emitted by modern generator -->
</userPreference>
</userPreferences>
<!-- Section 2: package-level identifiers -->
<odeResources>
<odeResource>
<key>odeId</key>
<value>20260427090000PROJID</value> <!-- stable; never regenerated -->
</odeResource>
<odeResource>
<key>odeVersionId</key>
<value>20260427091500VERSID</value> <!-- new on every export -->
</odeResource>
<odeResource>
<key>exe_version</key>
<value>3.0</value> <!-- ODE_VERSION constant -->
</odeResource>
</odeResources>
<!-- Section 3: document metadata -->
<odeProperties>
<!-- Core metadata -->
<odeProperty><key>pp_title</key><value>Introduction to Biology</value></odeProperty>
<odeProperty><key>pp_subtitle</key><value>Unit 1</value></odeProperty>
<odeProperty><key>pp_author</key><value>Jane Doe</value></odeProperty>
<odeProperty><key>pp_description</key><value>First-year biology overview.</value></odeProperty>
<odeProperty><key>pp_lang</key><value>en</value></odeProperty>
<odeProperty><key>pp_license</key><value>creative commons: attribution - share alike 4.0</value></odeProperty>
<odeProperty><key>pp_licenseUrl</key><value>https://creativecommons.org/licenses/by-sa/4.0/</value></odeProperty>
<odeProperty><key>pp_keywords</key><value>biology, cells, ecology</value></odeProperty>
<odeProperty><key>pp_category</key><value>Science</value></odeProperty>
<odeProperty><key>pp_theme</key><value>base</value></odeProperty>
<odeProperty><key>pp_exelearning_version</key><value>4.0.0</value></odeProperty>
<!-- Export options: booleans serialized as "true"/"false" strings -->
<odeProperty><key>pp_addExeLink</key><value>true</value></odeProperty>
<odeProperty><key>pp_addPagination</key><value>false</value></odeProperty>
<odeProperty><key>pp_addSearchBox</key><value>true</value></odeProperty>
<odeProperty><key>pp_addAccessibilityToolbar</key><value>true</value></odeProperty>
<odeProperty><key>pp_addMathJax</key><value>false</value></odeProperty>
<!-- exportSource has no pp_ prefix (legacy compatibility) -->
<odeProperty><key>exportSource</key><value>true</value></odeProperty>
<odeProperty><key>pp_globalFont</key><value>default</value></odeProperty>
<!-- Custom content: HTML-escaped (stored in XML text nodes) -->
<odeProperty>
<key>pp_extraHeadContent</key>
<value><meta name="robots" content="index,follow"></value>
</odeProperty>
<!-- footer has no pp_ prefix (legacy compatibility) -->
<odeProperty>
<key>footer</key>
<value><p>© 2026 Jane Doe</p></value>
</odeProperty>
</odeProperties>
<!-- Excluded from odeProperties (not emitted): -->
<!-- odeIdentifier → odeResources/odeId -->
<!-- createdAt, modifiedAt → internal Yjs only -->
<!-- scormIdentifier, masteryScore → SCORM manifest -->
<odeNavStructures>
<!-- ... pages ... -->
</odeNavStructures>
</ode>