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
- 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.
- 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.
- 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.
- Validate the bundle with
scripts/validate_generated_website_bundle.py before any publish. This is blocking.
- 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.
- 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.
- 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.
1---2name: website-from-webdav3description: 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.4---56# Website From WebDAV78Use 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."910This 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.1112## Blocking Gate — A Local File Is Not a Valid Completion1314When 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:1516- a **live, verified** published site: the target route returns the same content as the local `generated/` output, or17- 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.1819Read `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.2021## First decisions2223Establish these before publishing anything:2425- 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.26- Target DAV collection, e.g. `/DAV/home/kidehen/sites/openlink/`.27- Public route, e.g. `https://linkeddata.uriburner.com/sites/openlink/`.28- Identity mode: SQL username/password, `isql` WebID-TLS, WebID-TLS plus delegation, WebDAV username/password, WebDAV mTLS, or WebDAV mTLS plus `On-Behalf-Of`.29- 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).3031## Workflow32331. 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`.342. 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.353. 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`.364. Validate the bundle with `scripts/validate_generated_website_bundle.py` before any publish. This is blocking.375. 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`.386. 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.397. 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.4041## Mode references4243- For route bootstrap and `isql` TLS/WebID syntax, read `references/isql-route-mode.md`.44- For WebDAV publication and its out-of-scope boundary, read `references/webdav-publish-mode.md`.45- For the pipeline boundary (what this skill does and does not touch in `rdf/`), read `references/rdf-content-mode.md`.46- For why live SPARQL/VSP rendering was not chosen, read `references/webdav-website-engine-gate.md`.47- For agent-callable operations, read `references/opal-tool-mode.md` — not implemented in v1; documented as the extension point.4849## Practical guardrails5051- 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.52- `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.53- Avoid `$1`, `$2`, and similar tokens in any SQL loaded through `isql`; they can trigger macro substitution.54- 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.55- 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.56- 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.57- Never write credentials into the skill, generated SQL, logs, or committed files.58- **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.