feat(import): add a VitePress importer - #42
Merged
Merged
Conversation
Adds convertVitepressToSite and registers it with the
/system/api/v1/site/import/:platform dispatcher as the "vitepress"
platform, so a VitePress documentation repository can be imported into a
HAXcms site (issue #2923).
How the import works
- Resolves the repository and its default branch through the GitHub API,
then reads the file tree once and works from that listing.
- Finds .vitepress/config.{mjs,mts,js,ts}, preferring a config under docs/
over one at the repository root, and reads themeConfig.sidebar, base and
title from it. The config is parsed, never executed: a small literal
reader walks the object and gives up on anything that is not plain data.
- Builds the outline from the sidebar, nesting sidebar groups as indented
children. When the config cannot be read, or names no pages, the outline
falls back to the markdown file tree.
- Renders each page with markdown-it plus the container and footnote
plugins VitePress itself uses, so ::: containers and footnotes survive.
Frontmatter supplies the title, description and license.
HAX element mapping
- images -> media-image (source, alt)
- <VideoEmbed> -> video-player (source, media-title, caption)
- ::: learning-objective, assessment, practice, learning-component and
instructional-pattern containers -> oer-schema (typeof, oer-property)
- a per page license-element from frontmatter or the site license
Assets and links
- Assets referenced by pages are collected into build.files as raw URLs for
createSite to stage. Names are de-duplicated, and only the extensions
createSite accepts are collected.
- Relative links are resolved against the page that contains them and
rewritten to the imported slug, with the configured base prefix stripped.
GitHub API requests send a User-Agent, without which GitHub answers 403 to
every call; this follows the convention already documented in
convertGitbookToSite. Limits mirror the OpenStax importer: 500 pages, 2000
files, a 900 second fetch budget and a 100ms delay between requests.
Tests
test/unit/convertVitepressToSite.test.cjs adds 43 unit tests covering
outline building and both fallbacks, the page pipeline, asset collection
and link rewriting, the User-Agent, the limits and every error path. The
network is stubbed at safeFetch with an in-memory fixture repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
Member
|
@SanikaA3 excellent! next up as follow ups to this would be:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs haxtheweb/issues#2923, implementing the plan in this comment. This is step 1 of 3: the converter, the dispatcher case and the OpenAPI entry. The
@system/vitepressToSitemicro-frontend registration (webcomponents) and the CLI wiring (create) follow as their own PRs, the same split as #40.What it does
src/systemRoutes/v1/routes/imports/convertVitepressToSite.jstakes a VitePress repository URL and returns{ items, filename, files, site, truncated, unmappedComponents }, the shape the other converters in the directory return. (The plan listssiteFilesin the return shape; no converter here emits one, so this follows the code.)The outline comes out of the config, not a
SUMMARY.mdThe repository and its default branch are resolved through the GitHub API and the file tree is read once.
.vitepress/config.{mjs,mts,js,ts}is located in that tree, preferring a config underdocs/over one at the repository root, thenthemeConfig.sidebar,base,title,description,license,defaultAuthorandworkTitleare read out of it.The config is parsed, never executed: a small literal reader walks the object and returns nothing for anything that is not plain data — a spread, a function call, an import, a template literal. Sidebar groups become indented children,
link: '/'maps toindex.md. When the config cannot be read, or parses but names no pages, the outline falls back to the markdown file tree the way VitePress's own auto-sidebar does.Pages
Each page is rendered locally with
markdown-itplusmarkdown-it-containerandmarkdown-it-footnote— the two plugins the source repository itself uses, added as dependencies (10 lines of lockfile, no other churn). Frontmatter is split off first and supplies title, description, license and author.<media-image source="files/x.png" alt="…"><VideoEmbed src type title caption><video-player source media-title>with aslot="caption"::: learning-objective,assessment,practice,learning-component,instructional-pattern<oer-schema typeof oer-property>using the matching vocabulary,skill/forCourse/additionalType/gradingFormat/assessingcarried as propertieslicense, elsethemeConfig.license<license-element license title creator source>per pageunmappedComponentsEvery item carries
metadata.vitepress(repo, branch, path, license, author, workTitle, accessed) and ametadata.sourcepointing at the file on GitHub, the way the OpenStax import carriesmetadata.openstax.Assets and links
Media referenced by pages is collected into
build.filesasfiles/<name>→ raw URL andcreateSitestages it. That is only possible because of #41 (haxtheweb/issues#3060): until that landedcreateSiterejected URL-valuedbuild.files, which is why the OpenStax importer downloads and stages every image itself. This importer hands over URLs and letscreateSitefetch them through its SSRF-guarded staging path, so an import that is never turned into a site writes nothing to disk.Both VitePress reference styles are resolved —
/assets/…and../assets/…against the docs root and the page that contains them, anddocs/public/x.pngreferenced from the site root as/x.png. Names are de-duplicated (x.png,x-1.png) and limited to the extensions the bulk import accepts. Links between pages are resolved against the containing page and rewritten to the imported slug, and the configuredbase(/dmd-100-book/) is stripped, since HAX manages its own.The plan's open questions, as implemented
<video-player>per embed. Notmedia-playlist: praw pairsmedia-playlistwithaudio-playeras the audio pattern. Local.webm/.mp4are collected intobuild.filesand played fromfiles/; YouTube and Vimeo sources stay remote, whichvideo-playeralready handles.markdown-it-footnote's own output — numbered references linking to an end-of-page list with back-links. There is no footnote element inwebcomponentsto map to, and this output is plain editable HTML.site.licensefor the site and a<license-element>closing each page with its title, creator and source. Frontmatter wins overthemeConfig, andcc-by-sastyle codes are normalized to theby-sacodeslicense-elementtakes.unmappedComponents, so "log unmapped constructs" is visible to the caller instead of buried in a log.Q1 (how the CLI reaches the endpoint) is answered in step 3, the same way as OpenStax: option (a),
@system/vitepressToSiteresolved throughMicroFrontendRegistryConfig.base.The GitHub API needs a
User-AgentThe first live run got a 403 on every call. GitHub rejects API requests without a
User-Agent;convertGitbookToSitedocuments this and sendsHAXcms-Import/1.0, so this importer sends the same header and a test pins it.Worth flagging separately:
convertNotionToSitesends noUser-Agenton its GitHub calls, so it cannot be reading anything from that API today. Not touched here.Safety valves
500 pages, 2,000 files, a 900 second budget and 100 ms between requests, in one exported
LIMITSobject, matching the OpenStax importer. When a limit bites the response carriestruncated: trueand the pages that were not fetched link to their source instead of arriving empty.Testing
test/unit/convertVitepressToSite.test.cjsadds 43 tests with the network stubbed atsafeFetchagainst an in-memory fixture repository, so the suite never leaves the machine: request validation and every error path, config discovery including thedocs/preference, sidebar nesting, both fallbacks (unparseable config, and a config that parses but names no pages), frontmatter, the license normalization and per-page element, each OER container, footnotes,VideoEmbedlocal and remote, images and alt text, link rewriting andbasestripping, unmapped components, duplicate file names, theUser-Agent, and each limit.Live against
dmd-program/dmd-100-book, the repository in the issue:media-image, 13video-player, 104license-element, 46 footnote references; no:::,VideoEmbedor{{left in the output; 0 orphaned items, 0 duplicate slugs, 0 file keyscreateSitewould reject.createSiteover HTTP: 200, with 56 requested = 56 saved = 56 file entities, 51 of 51 pages with references linked to their entities, 0 references without a file on disk, and the staging directory empty afterwards.🤖 Generated with Claude Code