From 7ca9c62ea840cd4ffec6183bfd3bb8ad86438070 Mon Sep 17 00:00:00 2001 From: Aleksandr Shevchuk Date: Fri, 13 Nov 2026 14:10:07 +0200 Subject: [PATCH] The site offers the h5p-author skill as zips to install in Claude and ChatGPT vite.config.ts writes two zips into /h5p/skill/ from the pinned author package, the files that folder serves: h5p-author.zip, the skill's folder at its root, which Claude's app takes, and h5p-author-chatgpt.zip, the same files under skills/h5p-author/ with an Agent Plugins plugin.json, which ChatGPT's Plugins take. Every entry has one date, so a pin writes the same bytes each build. @zip.js/zip.js becomes a direct dependency, at the version the lockfile already held. The guide gets "Or install the skill once": Claude, ChatGPT, Open WebUI and agents with a terminal. The skill page and the README point to it. Closes #40. --- README.md | 2 +- package.json | 3 +- pnpm-lock.yaml | 3 ++ src/h5p/create-with-ai/index.njk | 38 +++++++++++++- src/h5p/skill/index.njk | 4 +- vite.config.ts | 90 +++++++++++++++++++++++++++++++- 6 files changed, 135 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 538ac40..a82997b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The site of the [missing-elements](https://github.com/missing-elements) web comp | `/` | The project list, the tools (the build page, which links the skill's page), and a footer of links | [src/index.njk](src/index.njk); the lists are in [src/_data/site.js](src/_data/site.js) | | `/pdfjs-viewer/` | The docs of ``, with a live preview | [src/pdfjs-viewer/](src/pdfjs-viewer/) | | `/h5p/build/` | The build page, for people: what it makes, a sample that plays, and the build of a chat's source into a `.h5p`, in the visitor's browser | [src/h5p/build/](src/h5p/build/) | -| `/h5p/skill/` | The skill's files, and a page that lists each at its full address, for a chat | [src/h5p/skill/](src/h5p/skill/), copied by [eleventy.config.js](eleventy.config.js) | +| `/h5p/skill/` | The skill's files, and a page that lists each at its full address, for a chat; and the skill as zips to install: `h5p-author.zip` for Claude, `h5p-author-chatgpt.zip` for ChatGPT's Plugins | [src/h5p/skill/](src/h5p/skill/), copied by [eleventy.config.js](eleventy.config.js); the zips written by [vite.config.ts](vite.config.ts) | | `/h5p/bundles/` | The skill's build tool and a library bundle per content type, for a chat that runs Node but cannot reach npm | written by [vite.config.ts](vite.config.ts) | | `/h5p/build/types/` | The skill's type files, which the build page copies for the chat | written by [vite.config.ts](vite.config.ts) | diff --git a/package.json b/package.json index d4e8d8f..0f92b2c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@missing-elements/h5p-libraries": "1.1.0", "@missing-elements/h5p-offline-player": "1.6.0", "@missing-elements/h5p-runtime": "1.0.1", - "@missing-elements/pdfjs-viewer": "4.5.0" + "@missing-elements/pdfjs-viewer": "4.5.0", + "@zip.js/zip.js": "2.24.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e92594c..ca65990 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@missing-elements/pdfjs-viewer': specifier: 4.5.0 version: 4.5.0 + '@zip.js/zip.js': + specifier: 2.24.0 + version: 2.24.0 devDependencies: '@11ty/eleventy': specifier: 3.1.8 diff --git a/src/h5p/create-with-ai/index.njk b/src/h5p/create-with-ai/index.njk index 451b565..d5a8f8c 100644 --- a/src/h5p/create-with-ai/index.njk +++ b/src/h5p/create-with-ai/index.njk @@ -12,6 +12,7 @@ socialImage: alt: "The build page: its headline, Build an H5P activity with AI, and a course presentation playing beside it." # The top bar's links to the page's own sections (topbar.njk). topbarLinks: + - { href: "#install", text: "Install" } - { href: "#prompts", text: "Prompts" } - { href: "#types", text: "Types" } - { href: "#open-source", text: "Open source" } @@ -39,7 +40,8 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o H5P. It writes the activity, and the build page makes it a .h5p file for Moodle or any other H5P site. Both are free and open source, with no account and nothing to install: you need only a - chat that can open a web page. + chat that can open a web page. In Claude or ChatGPT you can also + install the skill once.

Open the build page

@@ -111,6 +113,40 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o

+ {#- The zips are written by vite.config.ts from the release the site pins, so each holds the skill + the prompt above sends a chat to. Each was uploaded as it is, by the owner, on 2026-11-13. -#} +
+

Or install the skill once

+

+ Installed, the skill stays in your chat: ask for an activity with no address to paste, such as + "Use the h5p-author skill to make a quiz of five questions on the rivers of Europe". +

+
    +
  • + Claude, on claude.ai or in its apps: download + h5p-author.zip and upload it under Customize, + then Skills, with code execution on. Claude builds the .h5p file in the chat. +
  • +
  • + ChatGPT: download + h5p-author-chatgpt.zip and upload it + under Plugins. ChatGPT cannot install the build tool, so it hands over a link to the build + page. If the page says the link arrived damaged, ask for the link again, or for the source + as a file, and open that on the build page. +
  • +
  • + Open WebUI: an administrator imports the skill under Import from URL, from + its folder on GitHub, + https://github.com/missing-elements/h5p-offline-player/tree/main/skills/h5p-author. +
  • +
  • + An agent with a terminal, such as Claude Code, Codex or Cursor: + npx skills add missing-elements/h5p-offline-player --skill h5p-author. +
  • +
+

The zips hold h5p-author {{ skill.version }}, the release the build page runs.

+
+

Prompts to start from

diff --git a/src/h5p/skill/index.njk b/src/h5p/skill/index.njk index f4b83d8..265ae68 100644 --- a/src/h5p/skill/index.njk +++ b/src/h5p/skill/index.njk @@ -38,7 +38,9 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o

Where skills can be installed, install it instead: - npx skills add missing-elements/h5p-offline-player --skill h5p-author. + npx skills add missing-elements/h5p-offline-player --skill h5p-author, or in + Claude's or ChatGPT's app from the zips at + {{ site.url }}/h5p/create-with-ai/#install.

diff --git a/vite.config.ts b/vite.config.ts index b707cc1..f461574 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { basename, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { buildPackage, writeBundles } from '@missing-elements/h5p-author' +import { Uint8ArrayReader, Uint8ArrayWriter, ZipWriter } from '@zip.js/zip.js' import { build, defineConfig, type Plugin } from 'vite' /* @@ -202,12 +203,99 @@ function bundles(): Plugin { } } +/** One date for every entry, as `h5p-author bundle` writes its zips: the same pin writes the same bytes. */ +const ZIP_DATE = new Date(2000, 0, 1, 2) + +/** + * The h5p-author skill as zips to install, beside the files `/h5p/skill/` serves, from the same + * pinned author package: + * + * - `h5p-author.zip`: the skill's folder at the zip's root, `h5p-author/SKILL.md`, which Claude's + * app takes under Customize, Skills (as does Open WebUI's import). + * - `h5p-author-chatgpt.zip`: the same files as a plugin, under `skills/h5p-author/`, with its + * `plugin.json` at the root, which ChatGPT's Plugins take. A zip with the folder at its root is + * refused there ("Plugin archive must contain … `skills/*\/SKILL.md`"). The manifest is the Agent + * Plugins one, which allows no field but its own ten, with ChatGPT's name for the plugin under + * `extensions`; a name that leads with "H5P" says who made it. + * + * Both were uploaded as they are written here, by the owner on 2026-11-13. ChatGPT's code cannot + * read a plugin's files, so neither carries the build tool: where a chat cannot install it, the + * build page builds. + */ +function skillZips(): Plugin { + const author = new URL('./node_modules/@missing-elements/h5p-author/', import.meta.url) + const guide = 'https://missing-elements.github.io/h5p/create-with-ai/' + let outDir = '' + return { + name: 'missing-elements:skill-zips', + apply: 'build', + configResolved(config) { + outDir = resolve(config.root, config.build.outDir) + }, + async writeBundle() { + const { version } = JSON.parse(await readFile(new URL('package.json', author), 'utf8')) as { version: string } + // The files eleventy.config.js copies to /h5p/skill/: SKILL.md, the licence and references/. + const references = readdirSync(new URL('references/', author), { recursive: true, withFileTypes: true }) + .filter((entry) => entry.isFile()) + .map((entry) => join(entry.parentPath, entry.name).slice(fileURLToPath(author).length).split('\\').join('/')) + const files = ['SKILL.md', 'LICENSE', ...references].sort() + const skill = await Promise.all(files.map(async (path) => [path, await readFile(new URL(path, author))] as const)) + const manifest = { + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: 'h5p-author', + version, + description: 'Create H5P content from a prompt, a lesson or supplied material, as a playable .h5p: quizzes, flash cards, presentations, interactive books and more.', + author: { name: 'Missing Elements', url: 'https://github.com/missing-elements' }, + homepage: guide, + repository: 'https://github.com/missing-elements/h5p-offline-player', + license: 'MIT', + keywords: ['h5p', 'quiz', 'e-learning', 'authoring'], + extensions: { + 'com.openai': { + interface: { + displayName: 'H5P Author by Missing Elements', + shortDescription: 'Make a playable H5P activity from a prompt or your material', + developerName: 'Missing Elements', + websiteURL: guide + } + } + } + } + const folder = join(outDir, 'h5p', 'skill') + await mkdir(folder, { recursive: true }) + await writeFile(join(folder, 'h5p-author.zip'), await zipOf('h5p-author/', skill)) + await writeFile(join(folder, 'h5p-author-chatgpt.zip'), await zipOf('skills/h5p-author/', skill, [['plugin.json', new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`)]])) + } + } +} + +/** + * A zip of the files under `prefix`, after the files at its root, with an entry for each folder + * before what is in it, as `git archive` writes one: the shape of the zips first uploaded. + */ +async function zipOf(prefix: string, files: ReadonlyArray, atRoot: ReadonlyArray = []): Promise { + const writer = new ZipWriter(new Uint8ArrayWriter(), { useWebWorkers: false, lastModDate: ZIP_DATE, extendedTimestamp: false }) + for (const [path, bytes] of atRoot) await writer.add(path, new Uint8ArrayReader(bytes)) + const folders = new Set() + for (const [path, bytes] of files) { + const parts = `${prefix}${path}`.split('/') + for (let depth = 1; depth < parts.length; depth++) { + const name = `${parts.slice(0, depth).join('/')}/` + if (folders.has(name)) continue + folders.add(name) + await writer.add(name, undefined, { directory: true }) + } + await writer.add(`${prefix}${path}`, new Uint8ArrayReader(bytes)) + } + return writer.close() +} + export default defineConfig({ publicDir: false, // A missing file is a 404, as on GitHub Pages, not the home page: `pnpm preview` answers as the // site does, where an SPA's fallback would answer every address with a page. appType: 'mpa', - plugins: [emptyOwnFolders(), licenceFiles(), typeFiles(), sample(), bundles()], + plugins: [emptyOwnFolders(), licenceFiles(), typeFiles(), sample(), bundles(), skillZips()], build: { outDir: '_site', emptyOutDir: false,