Extensions¶
H5P defines how a question is played, not how it is organized. h5pkit adds a small, documented metadata block to each question for organization, difficulty, skills, and standards. Any tool can read and write it.
Where the block lives¶
The block sits under extensions.h5pkit, a sibling of a question's library
and params:
{
"library": "H5P.MultiChoice 1.16",
"params": { "question": "What is 3/4 + 1/8?", "answers": [] },
"extensions": {
"h5pkit": {
"section": "Fractions",
"topic": "Adding unlike denominators",
"tags": ["fractions", "addition"],
"difficulty": "medium",
"difficultyScore": 0.6,
"skills": [{ "taxonomy": "example-skills", "id": "fractions.add.unlike" }],
"standards": [{ "framework": "example-framework", "code": "5.NF.A.1" }],
"questionNumber": 4,
"learningObjective": "Add fractions with unlike denominators",
"source": "Unit 3"
}
}
}
Because the block is outside params, an H5P player never sees it. Players
ignore unknown fields, so content with the block plays everywhere content
without it does.
Fields¶
| Field | Type | Meaning |
|---|---|---|
section |
string | The section or category the question belongs to |
topic |
string | A more specific topic within the section |
tags |
string array | Free-form labels |
difficulty |
string | A display label, such as easy, medium, or hard |
difficultyScore |
number, 0 to 1 | Difficulty relative to the other questions in the same bank |
skills |
array of {taxonomy, id} |
Skills the question exercises |
standards |
array of {framework, code} |
Standards the question is aligned to |
questionNumber |
integer | Explicit ordering within a question set |
learningObjective |
string | What the question tests |
source |
string | Where the content came from |
scoring |
object | How a question H5P cannot score is scored; see Likert questions |
custom |
object | Consumer-private data, never interpreted by h5pkit |
Every field is optional.
The relative-difficulty rule¶
difficultyScore ranks a question against the other questions in the same
bank: 0 is the easiest question in the bank and 1 the hardest. It is not an
absolute scale, and two banks' scores are not comparable. A score of 0.8 in a
beginner bank and 0.8 in an advanced bank say nothing about each other.
Use difficulty for a label a person reads, and difficultyScore for
ordering and selection within a bank. A question without a score is
unscored, not easy.
Skills and standards¶
A skill or standard is always a pair: the scheme it comes from and its identifier in that scheme. h5pkit does not ship or validate any taxonomy or framework; it only requires both parts to be present. Pick stable identifiers, because banks compute and filter on these pairs.
Custom¶
custom is an escape hatch for data that only one consumer understands.
h5pkit and its registry carry it unchanged and never read it. If a field in
custom turns out to be generally useful, propose it as a documented field
instead.
Likert questions¶
H5P has no Likert content type, and its Questionnaire type does not score.
h5pkit writes a Likert item as a standard H5P.MultiChoice question and
records how to score it in scoring:
{
"library": "H5P.MultiChoice 1.16",
"params": {
"question": "I make room for experiments even when success is uncertain.",
"answers": [
{ "text": "Strongly disagree", "correct": true },
{ "text": "Disagree", "correct": true },
{ "text": "Neutral", "correct": true },
{ "text": "Agree", "correct": true },
{ "text": "Strongly agree", "correct": true }
],
"behaviour": {
"type": "single",
"randomAnswers": false,
"enableCheckButton": false,
"enableSolutionsButton": false,
"enableRetry": false,
"showScorePoints": false
}
},
"extensions": {
"h5pkit": {
"skills": [{ "taxonomy": "example-skills", "id": "innovation" }],
"scoring": { "method": "likert", "scale": "agreement-5" }
}
}
}
The question plays in any H5P player as an ungraded single choice. Every
answer is marked correct only because H5P.MultiChoice requires a correct
answer; the score H5P reports for it is meaningless and should be ignored.
scoring field |
Meaning |
|---|---|
method |
likert, the only method |
scale |
An identifier for the answer scale, such as agreement-5, so questions on the same scale can be compared |
reverse |
true for a reverse-keyed item |
The value of a response is the chosen answer's position on the scale, 1 to N, in the order the answers are listed. For a reverse-keyed item it is N+1 minus the position. Answer order is the scale, which is why the question must not randomize answers.
A Likert question must:
- Be
H5P.MultiChoice, with at least two answers, all marked correct. - Set
behaviour.typetosingle. Underauto, several correct answers become checkboxes. - Set
behaviour.randomAnswerstofalse. H5P randomizes by default. - Set
behaviour.enableCheckButtontofalse, so the player never shows every answer as correct.
Building and scoring in Go¶
labels := []string{"Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree"}
skill := h5p.SkillRef{Taxonomy: "example-skills", ID: "innovation"}
qs, err := h5p.NewQuestionSetBuilder().
SetTitle("Reflection").
AddLikertQuestion("I make room for experiments.", "agreement-5", labels, false,
h5p.NewExtension("Innovation", 1).WithSkills(skill)).
AddLikertQuestion("I prefer proven approaches.", "agreement-5", labels, true,
h5p.NewExtension("Innovation", 2).WithSkills(skill)).
Build()
if err != nil {
return err
}
if err := qs.Validate(); err != nil {
return err
}
// Responses map a question's index to the chosen answer's index.
scores, err := qs.ScoreLikert(map[int]int{0: 3, 1: 1})
if err != nil {
return err
}
fmt.Println(scores[skill].Mean) // 4 on a 1 to 5 scale
fmt.Println(scores[skill].Normalized) // 0.75 on a 0 to 1 scale
ScoreLikert groups responses by skill. A question with several skills
counts toward each. Normalized maps every value to 0 to 1 before averaging,
so skills scored on scales of different sizes can be compared.
Validation¶
Extension.Validate checks that difficultyScore, when present, is between
0 and 1; that every skill has a taxonomy and an id and every standard a
framework and a code; and that scoring, when present, has the likert
method and a scale. QuestionSet.Validate runs it for every question, and
for a question with Likert scoring also checks the Likert rules above.
Building the block in Go¶
ext := h5p.NewExtension("Fractions", 4).
WithTopic("Adding unlike denominators").
WithTags("fractions", "addition").
WithDifficulty("medium").
WithDifficultyScore(0.6).
WithSkills(h5p.SkillRef{Taxonomy: "example-skills", ID: "fractions.add.unlike"}).
WithStandards(h5p.StandardRef{Framework: "example-framework", Code: "5.NF.A.1"}).
WithLearningObjective("Add fractions with unlike denominators")
question := h5p.NewMultiChoiceQuestionWithExtensions(params, ext)
if err := ext.Validate(); err != nil {
return err
}
Extensions.Extension() returns the block from a question, or nil when it
has none.
The h5pkit key¶
The block is read and written only under extensions.h5pkit. Earlier
releases used extensions.h5pGo; that key is no longer read, so rename it in
any content that still carries it.
Guidance for other tools¶
A tool that reads or writes H5P content should:
- Preserve
extensionswhen it rewrites a question, even if it does not understand the block. - Read and write the block only under
h5pkit. - Treat a missing field as unknown, not as zero or empty.
- Never move the block into
params, where it would reach the player. - Leave
customuntouched.