# Website From Webdav

> Publish a static HTML/CSS/JS website (generated by an RDF-Turtle + SHACL + Jinja2 pipeline, e.g. sites/openlink/rdf/build.py) to a Virtuoso WebDAV root and map a public route to it via isql/VHOST_DEFINE. Use for deploying or updating an already-built OpenLink-style portfolio site onto a WebDAV target, verifying a live route serves the generated content, or producing a deployable bundle when live credentials aren't available. Triggers on phrases like "publish this site to WebDAV", "deploy the generated site", "set up an initial OpenLink site from a WebDAV root", "is this site live", or any request to push a pre-rendered static site onto a Virtuoso WebDAV/VHOST target. Does not author or edit RDF content, run the SHACL/Jinja2 pipeline's internal logic, or render pages live from SPARQL — see references/rdf-content-mode.md for the boundary.

- Skill: `openlinksoftware/website-from-webdav` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add openlinksoftware/website-from-webdav`
- Raw SKILL.md: https://api.skillmd.com/api/skills/openlinksoftware/website-from-webdav/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: OpenLinkSoftware (https://skillmd.com/u/openlinksoftware)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/openlinksoftware/website-from-webdav

---


# Website From WebDAV

Use this skill to take a **static, already-generated** HTML/CSS/JS site — the output of an RDF-Turtle + SHACL + Jinja2 pipeline such as `sites/openlink/rdf/build.py` — and publish it to a Virtuoso WebDAV root with a mapped public route. The site's content model (ontology, SHACL shapes, ordering) is authored and validated upstream of this skill; this skill's job starts at "here is a `generated/` folder of finished HTML, get it live."

This is deliberately narrower than `weblog-from-webdav` or `osdi-inclusion-engine`: those skills render content live on every request (WebDAV-collection enumeration inside a bespoke VSP, or a shared XSLT skin wrapping WebDAV-sourced HTML). This skill's content is already fully rendered before it arrives — publishing it is a plain WebDAV PUT plus a static-file `VHOST_DEFINE`, no VSP execution and no live SPARQL query involved. See `references/webdav-website-engine-gate.md` for why that choice was made and when it would change.

## Blocking Gate — A Local File Is Not a Valid Completion

When the user asks to publish, deploy, or "make live" a generated site, a description of what *would* happen, or a claim that files exist locally, is **not** a valid completion unless the user explicitly asks only for a local/preview check. The default deliverable is either:

- a **live, verified** published site: the target route returns the same content as the local `generated/` output, or
- a **complete deployable bundle**: `generated/*.html`, the site's `assets/` (`site.css`, `theme.js`), a path-substituted `templates/deploy-website-route.sql`, and a README/run-notes file — with `scripts/validate_generated_website_bundle.py` passing against it.

Read `references/webdav-website-engine-gate.md` before claiming either. If deployment credentials (WebDAV and/or `isql`) are unavailable, stop at the deployable bundle and say plainly that live publication is blocked by missing authenticated access — do not describe hypothetical success.

## First decisions

Establish these before publishing anything:

- Source pipeline directory (e.g. `{LLM_ROOT}/Claude Generated/sites/openlink/`) and whether `rdf/build.py` needs to be (re)run first, or `generated/` is already current.
- Target DAV collection, e.g. `/DAV/home/kidehen/sites/openlink/`.
- Public route, e.g. `https://linkeddata.uriburner.com/sites/openlink/`.
- Identity mode: SQL username/password, `isql` WebID-TLS, WebID-TLS plus delegation, WebDAV username/password, WebDAV mTLS, or WebDAV mTLS plus `On-Behalf-Of`.
- Whether this is a first-time route bootstrap (`isql` + `deploy-website-route.sql` required) or a content-only republish of an existing route (WebDAV publish only, per `references/webdav-publish-mode.md`'s out-of-scope boundary — it never touches route configuration).

## Workflow

1. Confirm the source `generated/` output is current: if asked to (re)build, run the pipeline's own `rdf/validate_shacl.py` then `rdf/build.py` unchanged — this skill does not reimplement or bypass that validation. See `references/rdf-content-mode.md`.
2. Before writing anything, search prior memory/work for `validate_generated_weblog_bundle`, `VHOST_DEFINE`, `DAV_RES_UPLOAD_STRSES_INT`, `website-from-webdav`; reuse proven fixes rather than rediscovering them.
3. Determine whether the target route already exists (query `DB.DBA.HTTP_PATH` and `WS.WS.SYS_DAV_RES` for the target path via `isql`). If not, bootstrap it: patch `templates/deploy-website-route.sql` with the real DAV path/route/title, then run it with `isql`. See `references/isql-route-mode.md`.
4. Validate the bundle with `scripts/validate_generated_website_bundle.py` before any publish. This is blocking.
5. Publish via WebDAV using `scripts/publish_static_bundle.py` — PUTs `generated/*.html` and `assets/*` recursively, verifies each write with a follow-up GET/PROPFIND compare. See `references/webdav-publish-mode.md`.
6. Verify: fetch the live route, confirm the served bytes match the local `generated/` file (allowing only whitespace/attribute-order differences, same tolerance the pipeline's own Phase 1 verification uses), confirm `assets/site.css`/`theme.js` load with correct MIME types.
7. Report exactly what happened: route bootstrapped or reused, files published with their DAV paths, verification result — or, if credentials were missing, the deployable bundle location and the explicit statement that live publication did not happen.

## Mode references

- For route bootstrap and `isql` TLS/WebID syntax, read `references/isql-route-mode.md`.
- For WebDAV publication and its out-of-scope boundary, read `references/webdav-publish-mode.md`.
- For the pipeline boundary (what this skill does and does not touch in `rdf/`), read `references/rdf-content-mode.md`.
- For why live SPARQL/VSP rendering was not chosen, read `references/webdav-website-engine-gate.md`.
- For agent-callable operations, read `references/opal-tool-mode.md` — not implemented in v1; documented as the extension point.

## Practical guardrails

- Never run `rdf/build.py`'s SHACL validation with warnings suppressed or `--skip-validate`-style shortcuts; a failing gate means the content is not ready to publish, not an obstacle to route around.
- `DB.DBA.DAV_RES_UPLOAD_STRSES_INT` (if used for any non-static-file step) needs a `string_output()` stream populated via `http(content, stream)`, not a plain string.
- Avoid `$1`, `$2`, and similar tokens in any SQL loaded through `isql`; they can trigger macro substitution.
- A static-file `VHOST_DEFINE` uses `is_dav=>1`, `is_brws=>0`, `def_page=>'index.html'` — do not set `def_page=>'index.vsp'` unless a VSP entry point genuinely exists; a `def_page` pointing at a nonexistent VSP resource serves raw source or a 404, not the site.
- Delete any prior conflicting `VHOST_DEFINE` for the same host/path before redefining it, same as `weblog-from-webdav`'s route-remove-then-redefine pattern.
- Treat the certificate-bearing identity and the delegated principal as separate facts: `-X`/`--cert` identifies the calling agent, `-W`/`On-Behalf-Of` identifies the WebID principal whose ACL rights should be evaluated.
- Never write credentials into the skill, generated SQL, logs, or committed files.
- **Remote uploads and removals** follow the same HTTP → WebDAV → iSQL order and principal PKCS#12 pattern (`howto/remote-webdav-upload.ttl`, `howto/remote-resource-removal.ttl`, prefs Steps 215–216). URIBurner writes/deletes go to port 5443. Report `:5443` GET and public 443 GET separately after PUT; after DELETE, verify the user-named URL, slash variants, `:5443` GET, and the folder listing before claiming removal.

