weblog-from-webdav
Skill42 starsChanged 50 days agoMITView on GitHub
Build, deploy, update, or troubleshoot a Virtuoso Server Pages weblog generated from a Virtuoso WebDAV collection of HTML or Markdown documents. Use for VSP weblog engine setup checks, SQL bootstrap via Virtuoso isql or isql over TLS/WebID, VSP weblog templates, RSS/Atom feed discovery, AtomPub links, scoped full-text search, calendar date filtering, schema:category facets, category metadata application, and post publication via WebDAV copying of HTML, Markdown, and asset folders including mTLS and On-Behalf-Of delegation.
In this repository45 other files it gives its agents.
- agent-rdf-memory/AGENTS.mdAGENTS.md
- AGENTS.mdAGENTS.md
- a2a-clientSkill
- acp-clientSkill
- csv-to-rdf-det-generatorSkill
- data-twinglerSkill
- dbpedia-query-skillSkill
- document-to-kg-skillSkill
- fediverse-crudSkill
- fuxi-engineerSkill
- infographic-describerSkill
- iodbc-dsn-managerSkill
- kg-generatorSkill
- linked-data-skillsSkill
- llm-routing-skillSkill
- monidSkill
- mpp-stripe-clientSkill
- mtls-curlSkill
- opal-agent-skill-assemblerSkill
- openlink-license-managerSkill
- openlink-license-readerSkill
- openlink-request-broker-configuratorSkill
- opml-rss-readerSkill
- osdi-inclusion-engineSkill
- pinchtabSkill
- rdf-det-variant-generatorSkill
- rdf-infographic-skillSkill
- resource-access-offers-generatorSkill
- rss-feed-generatorSkill
- s3-query-skillSkill
- screencast-recorderSkill
- set-webdav-resource-propertySkill
- sparql-faceted-browserSkill
- ucp-clientSkill
- uriburner-opal-agent-skillsSkill
- virtuoso-rdf-bulk-loaderSkill
- virtuoso-rdf-loaderSkill
- virtuoso-support-agentSkill
- wc2026-match-reportSkill
- website-from-webdavSkill
- wikidata-query-skillSkill
- world-cup-2026-navigatorSkill
- x402-buyerSkill
- xmla-clientSkill
- youidSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 3 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 12
When the user asks to generate a weblog from a WebDAV URL, a static HTML index is not a valid completion unless the user explicitly asks …
- Very specific: line 35
Look-and-feel is itself a configuration modality when using deploy-weblog-skinned.sql: weblog:skin (classic default, or editorial) resolves …
- Very specific: line 84
Before authoring VSP or SQL, search prior memory/work for DAV_RES_UPLOAD_STRSES_INT, string_output, HTTP_PATH, index.vsp, raw <?vsp, …
- Very specific: line 86
If the engine is missing or incomplete, switch to isql-engine setup.
- Very specific: line 89
Use scoped search. In Virtuoso SQL, keep contains / xcontains as a top-level AND predicate and escape multi-word user input into a valid …
- Very specific: line 90
Use calendar controls for date ranges in the sidebar, with from and to parameters preserved in filtered links.
- Very specific: line 110
Avoid $1, $2, and similar tokens in SQL loaded by isql.
- Very specific: line 111
Do not rely on SET MACRO_SUBSTITUTION OFF unless the target Virtuoso build accepts it.
- Very specific: line 113
Use dict_iter_next for category aggregate dictionaries.
- Distinctive: line 114
Keep feed links resolvable.
- Very specific: line 116
Treat the certificate-bearing identity and the delegated principal as separate facts: -X or --cert identifies the calling agent, while -W …
- Distinctive: line 118
Never write credentials into the skill, generated SQL, VSP, logs, or committed files.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Weblog From WebDAV
Use this skill to turn a Virtuoso WebDAV folder into a live weblog whose posts remain ordinary files. The skill always starts by determining whether the Virtuoso-side weblog engine is already in place. If it is not, bootstrap it with isql; WebDAV is then used for the post-publication workflow.
Blocking Gate — WebDAV URL Means VSP/isql Weblog
When the user asks to generate a weblog from a WebDAV URL, a static HTML index is not a valid completion unless the user explicitly asks for a static-only page or preview. The default deliverable is either:
- a live verified VSP weblog deployed on the target Virtuoso/WebDAV server, or
- a complete deployable VSP/isql bundle:
index.vsp,deploy-*.sql, optional route SQL, optional facet/category SQL when requested, README/run notes outside the skill package, and verification queries.
Before writing or claiming completion, read references/webdav-weblog-engine-gate.md and run scripts/validate_generated_weblog_bundle.py against the generated bundle. If deployment credentials are unavailable, stop at the deployable bundle and state that live deployment is blocked by missing authenticated isql/WebDAV access. Do not substitute a static HTML lens as the answer.
The skill supports two interaction modalities, each with plain credential and TLS/WebID variants:
- isql engine mode: inspect or create the server-side weblog engine: VSP resource deployment, DAV path mapping assumptions, route/friendly URL setup, SQL helpers, full-text/search support, feed handling, metadata access, ACL/cache maintenance, and category staging. If the SQL listener is TLS-enabled, use
isqlwith-Xfor the client PKCS#12 bundle,-Tfor the CA bundle, and-Wfor delegated WebID identity. - WebDAV post mode: publish posts and assets only, by copying HTML files, Markdown files, and associated asset folders into the configured DAV collection. WebDAV can also read or set custom resource properties when the engine already supports them. If acting as a software agent for a principal, pass the principal WebID via the
On-Behalf-OfHTTP header. - OPAL tool mode: expose server-side weblog operations, such as post pinning, as Virtuoso stored procedures registered with
OAI.DBA.REGISTER_CHAT_FUNCTION. Registered functions are available to OPAL/MCP-capable agents and are described through/chat/functions/openapi.yaml.
First decisions
Establish these before editing or deploying:
- Target DAV collection, for example
/DAV/www2.openlinksw.com/data/html/. - Public route, for example
https://www.openlinksw.com/weblog/. - Deployment mode:
isql-engine,webdav-posts, orhybridwhere SQL bootstraps the engine and WebDAV publishes posts. - Identity mode: SQL username/password, WebID-TLS client certificate, WebID-TLS plus delegation, WebDAV username/password, WebDAV mTLS, or WebDAV mTLS plus
On-Behalf-Of. - Variant: base weblog, OpenLink-site themed, facet-enabled, or the general-purpose, parameterized
templates/deploy-weblog-skinned.sql(any DAV collection/route, not fixed to one site). - Whether search, calendar date filtering, RSS/Atom/AtomPub links,
schema:categoryfacets, and scheduled category refresh are required. - Look-and-feel is itself a configuration modality when using
deploy-weblog-skinned.sql:weblog:skin(classicdefault, oreditorial) resolves at request time from a?skin=override, then aweblog:skincollection property, then the deploy-time default — no redeploy needed to switch. Seereferences/skin-authoring-contract.md. - Whether the newsletter feature (double opt-in subscribe, batched digest via the operator's own SMTP relay) is wanted:
weblog:newsletterEnabledplustemplates/register-weblog-newsletter.sql. - Every outbound newsletter message (confirm, admin-import activation notice, admin-unsubscribe notice, and both digest sends) carries
Message-ID,List-Id,Precedence: bulk, and — when a per-recipient unsubscribe link exists —List-Unsubscribe/List-Unsubscribe-Post(RFC 8058 one-click), built byWEBLOG_NEWSLETTER_BULK_HEADERS. These headers reduce spam-tagging risk but are not sufficient on their own: actual inbox placement is dominated by SPF/DKIM/DMARC alignment on the sending domain and the SMTP relay (weblog:newsletterSmtpServer/ the server's default mail server), which this skill has no way to configure from VSP/SQL — that is the operator's mail infrastructure to set up. - Email layout: every newsletter message is
multipart/alternative(plain text + HTML, each part base64-encoded so no line exceeds SMTP's 998-byte limit), with aSubjectRFC 2047-encoded when it contains non-ASCII. The HTML follows the Substack pattern and is built for mail clients, not browsers: a 600pxtable-layout:fixedcontainer, inline styles throughout (so a post's own stylesheet in full-content mode can't restyle it), a hidden preview line, a masthead linked to the weblog -- the one place the weblog's name appears in the body (the welcome heading is just "Welcome", the footer says "this weblog", and digest bylines omit it; the plain-text part, which has no masthead, keeps the name) -- and per post a serif title, the page's<meta name="description">as subtitle, a byline row (date · reading time · Read online), the body, and a table-based "Read the full post" button. Confirm / activation / unsubscribe notices render their admin-editable plain-text templates as paragraphs with a button for the primary link. The footer carries Unsubscribe, "Visit the weblog", and — whenweblog:newsletterPostalAddressis set on the collection — the sender's postal address (required for commercial mail under CAN-SPAM). A post body cut at the digest's length limit is passed throughWEBLOG_NEWSLETTER_CLOSE_OPEN_TAGS, because an element left open by the cut (e.g. adisplay:nonecontainer) otherwise hides every later post and the footer. Gmail clips messages over ~102 KB, which several full-content infographic posts in one digest will exceed;weblog:newsletterContentModeauto/snippetkeeps digests small. Logo: every email (confirm, welcome, unsubscribe notice, digest, immediate) shows a logo above the masthead, linked to the weblog, 160px wide, alt text = the sender name.weblog:emailLogoUrl(Email Server Config > Logo image URL): blank = OpenLink's logohttps://www.openlinksw.com/images/oplogo_std_200x80.png(200x80 PNG, transparent background -- the raster logo openlinksw.com publishes as itsdepiction; its header logo is SVG, which Gmail and Outlook do not render in email),none= no logo, otherwise an http(s) image URL (PNG/JPEG/GIF). Verified locally 2026-09-28 for the default, a custom URL,none, an invalid value being rejected, and the digest. weblog:adminEmail(settable from the Email Server Config panel) is the site operator's own contact address, distinct fromweblog:newsletterFromAddress(the subscriber-facing sender identity): it is used asReply-Toon every outbound newsletter message, and as the destination for operational alerts (a new confirmed subscriber, a digest batch with failed sends) viaWEBLOG_NEWSLETTER_NOTIFY_ADMIN. Leaving it blank disables both, matching this skill's soft-fail-on-missing-config convention.- Admin action security:
index.vsp'sadmin_actiondispatcher (used by every dashboard form — skin/newsletter config, digest send, import, pinning, etc.) is gated by real HTTP Digest authentication, not a shared-secret token. Becauseindex.vspitself must stay world-executable to serve the public blog,deploy-weblog-skinned.sqlmaps a second VHOST —action_route(derived as{public route}-action/, e.g./weblog-admin-action/) — at the same physicalindex.vsp, protected declaratively viaVHOST_DEFINE'sauth_fn=>'DB.DBA.WEBLOG_ADMIN_AUTH_FN'/sec=>'digest'/realm=>....WEBLOG_ADMIN_AUTH_FNwraps Virtuoso's nativeDB.DBA.HP_AUTH_SQL_USERbuiltin (real Digest verification againstSQL_USERS) and additionally checks the authenticated username against the collection'sweblog:adminDavUserproperty, so only that collection's configured admin — not just any valid SQL account — passes. The dispatcher itself also rejects anyadmin_actionrequest that didn't arrive viaaction_route(checked againsthttp_request_header()[0]), so the same code reachable through the public route can't be used to bypass the gate.dbais always accepted, the same way it bypasses ordinary DAV/SQL grants everywhere else; members of theWEBLOG_OPERATORrole are also accepted. Any account authorized this way must haveU_SQL_ENABLE=1—WEBLOG_ADMIN_AUTH_FNauthenticates throughDB.DBA.HP_AUTH_SQL_USER, which refuses an SQL-disabled account outright, even with the correct password (a DAV-only account like the built-indavuser, or a content account that was deliberately SQL-disabled, fails here even though it can read the Digest-protected admin dashboard — that check goes through ordinary DAV authentication, a separate mechanism with no such requirement). Every setting the dashboard saves (weblog:skin,weblog:adminEmail,weblog:newsletterConfirmBaseUrl, digest/content mode, per-postschema:category/schema:position, the default-pin seed at deploy time) is written withDB.DBA.DAV_PROP_SET_INT, never withDAV_PROP_SET (..., dav_user, pwd_magic_calc(...), ...)— the VSP always runs as the route's ownvsp_userregardless of who authenticated at the HTTP layer, so a write keyed to that account's own SQL password hash silently does nothing whenever that hash doesn't resolve (confirmed live:dbaauthenticating successfully on the action route still could not make a setting stick, because the VSP was still executing, and still writing, as a differentvsp_user).DAV_PROP_SET_INTneeds no account credential at all, so it is unaffected by any account'sU_SQL_ENABLEstate. RES_PERMScan silently reset on upload (blocking correctness requirement): confirmed live on a real instance thatDAV_RES_UPLOAD_STRSES_INT's perms argument did not take effect for eitherindex.vspordashboard.html—RES_OWNER/RES_GROUPapplied correctly, butRES_PERMScame back as a plain default (rw-r--r--, world-readable, no world-execute) instead of the requested value, on the same Virtuoso build (08.03.3335) that applies it correctly locally; the cause was not tracked down. Forindex.vspthis silently breaks the action route the same way the general restrictive-permissions constraint does (world falls back to read-only, so an authenticated request is served raw source instead of executed) — but herenobodyanddbaboth still get real execution, since owner/group already haverwx/r-x. Fordashboard.htmla reset is a direct exposure of subscriber names/emails, not just a broken feature. Both uploads now re-assertRES_OWNER/RES_GROUP/RES_PERMSwith an explicitUPDATE WS.WS.SYS_DAV_RESimmediately after the upload call, so the deploy's own word on permissions is never left to a write whose actual effect isn't being verified. There is noweblog:adminActionToken/shared-secret scheme anymore; a still-presentadmin_tokenhidden field on dashboard forms is vestigial (always empty) and does nothing.- Admin dashboard location (blocking architectural rule):
dashboard.htmllists subscriber names and emails, so it must never live in the public blog collection — anything there inherits whatever makes that collection public (on UB, an ACL on/DAV/demos/daas/left the file anonymously readable despite its own000world bits). Ask the blog admin where the admin dashboard should live, and pass it asWEBLOG_DAV_DEPLOY_SKINNED's optional 7th argument,admin_collection. If it is omitted, the location a previous deploy recorded inweblog:adminCollectionis reused. Otherwise the suggested default is an aunt/uncle of the blog collection (a sibling of its parent, so it's outside the public parent's subtree too):/DAV/demos/daas/→/DAV/demos-daas-admin/. The deploy refuses a location inside the blog collection. The admin collection is owned bydav_userand grouped to theWEBLOG_OPERATORrole, with no public read (111111000N;dashboard.htmlis110110000N). The permissions are re-applied on every deploy, and any olddashboard.htmlleft in the public collection is deleted. DAV owner/group arguments must be the role's numericU_ID: passing the role name silently stores group-12. The admin route is defined once, TLS-only, withppathset to the admin collection,sec=>'SSL',def_page=>'dashboard.vsp', andvsp_user=>dav_user(see VAL sign-in below;dashboard.htmlitself stays locked). Itsvhostcomes from the 8th argument,admin_host. When the deploy is driven from a prompt, takeadmin_hostfrom the host of the blog's public URL (for examplehttps://linkeddata.uriburner.com/weblog/→linkeddata.uriburner.com);[URIQA] DefaultHostis only the fallback. Itslhostcomes from the 9th argument,admin_listener(e.g.':443'), else[HTTPServer] SSLPort, else an existing:443listener. Instances whose TLS listener is defined in Conductor (UB) have noSSLPortin the ini. The deploy fails before writing anything if no host or TLS listener can be resolved. - WebDAV permalink and Subscribe button: the selected/viewed post (both skins) shows a "WebDAV Permalink" link — the post's raw underlying DAV URL (
site_base+weblog:dav_collection+ filename), not the weblog's own?post=query-string view — distinct and separately clickable. The feed-buttons nav gets a third "Subscribe" button (a matching--subscribecolor token per skin) wheneverweblog:newsletterEnabledis true. The subscribe form itself is a native<dialog id="newsletter">(.showModal()/.close(), with a close button and a dimmed::backdrop) opened in place by the Subscribe button — not a#newsletteranchor scroll to a footer section; verified live end-to-end (open, close, and a real subscribe submission creating apendingWEBLOG_SUBSCRIBERrow) via a real browser, since<dialog>interactivity can't be checked withcurl. - Email templates: every outbound email's subject, and (for confirm/activation/unsubscribe-notice, plus digest's optional intro paragraph) body, is admin-editable from the dashboard's Email Templates panel via
DB.DBA.WEBLOG_RENDER_EMAIL_TEMPLATE(reads aweblog:emailSubject*/weblog:emailBody*/weblog:emailIntroDigestproperty, falling back to the exact current hardcoded text when unset, then substitutes{{TOKEN}}s). The digest's per-post-card HTML and the immediate-mode body are generated code, not template-editable — too complex to safely expose as free text.admin_action=set_email_template(template_type,subject,body, orreset=1to revert) rejects a save that drops a type's required token (e.g. a confirm email missing{{CONFIRM_URL}}would have no way to actually confirm) viaDB.DBA.WEBLOG_CHECK_EMAIL_TEMPLATE_TOKENS, checked before anything is written. Verified live: default render, a customized save actually changing whatWEBLOG_RENDER_EMAIL_TEMPLATEreturns, a missing-required-token save correctly rejected, and reset restoring the default. Welcome email for imported subscribers (activation): placeholders{{NAME}}/{{FIRST_NAME}}("Subscriber" when the import carried no name),{{EMAIL}},{{ADMIN_NAME}}(weblog:adminName, the Email Server Config panel's Admin name, else the sender name),{{WEBLOG_TITLE}},{{WEBLOG_URL}},{{GREETING}};{{UNSUBSCRIBE_URL}}is optional because the HTML footer andList-Unsubscribeheader always carry the link and the plain-text part gets it appended when the message leaves it out. Each import form (CSV, RDF, Manual Entry) also has an optional "Welcome email for this import" subject/message used for that batch only (welcome_subject/welcome_body->WEBLOG_NEWSLETTER_IMPORT_CSV/_RDF/_ONE->WEBLOG_NEWSLETTER_SEND_ACTIVATION'ssubject_override/body_override); blank falls back to the saved template. Verified locally 2026-09-28 through the real Digest-authenticated admin forms: saved template with a named and an unnamed subscriber, and a per-import override. - Every dashboard panel collapses by default: Delivery Settings, Email Server Config, Email Templates, Import Subscribers, Tagging & Scheduling, and Subscribers are each a native
<details class="collapsible">(noopenattribute, no JS) — a freshly opened dashboard is a stack of collapsed titles, not a page of forms to scroll past. Email Templates and Tagging & Scheduling additionally nest their own inner<details>(the 6 template types; Post Tags & Pinning / Scheduled Jobs) — verified live that the outer and inner collapsibles toggle independently, since a naive tag-balance mistake across a hand-edited<section>/<details>pair is easy to introduce and easy to miss without actually rendering the result. Subscribers'<summary>carries a live count badge (Subscribers (N)) so the total is visible without expanding it. - Subscriber actions must refresh the dashboard too:
dashboard.htmlonly regenerates when an admin action runs, a scheduled auto-refresh fires, or the site is (re)deployed — a subscriber confirming or unsubscribing through the public route (WEBLOG_NEWSLETTER_SUBSCRIBE/CONFIRM/UNSUBSCRIBE) is none of those. Confirmed live: a subscriber who genuinely confirmed hours earlier, with no auto-refresh schedule configured, still showed as 0 subscribers on the dashboard. All three now callWEBLOG_NEWSLETTER_TRY_DASHBOARD_REFRESH(best-effort, never blocks the subscriber-facing action if it fails or the dashboard proc isn't installed) right after theWEBLOG_SUBSCRIBERrow changes. - Upgrades must never be destructive (blocking correctness requirement):
WEBLOG_DAV_DEPLOY_SKINNEDrefuses to overwrite an existingindex.vspunless its current content contains the literal stringmulti-skin, config-driven(present in everyindex.vspthis template has ever generated, unmodified by any{{TOKEN}}substitution) — verified live this correctly blocks overwriting a different, hand-authored template (e.g.deploy-weblog-opl-site*.sql) while still allowing a normal redeploy over an existing deploy-weblog-skinned.sql site. The 12th argument,allow_template_overwrite(default0), is the explicit, deliberate opt-in to bypass this and replace a non-matchingindex.vspanyway. A skin change on an already-deployed site never needs a redeploy at all —weblog:skin(the admin dashboard's Skin setting) is resolved at request time — so this default only ever blocks something that shouldn't have been attempted in the first place. site_base(scheme://host) — two bugs fixed together, both confirmed live: it silently resolved to empty (the 4-arghttp_request_header(lines, 'Host', '', '')keyword-lookup call never actually found the Host header despite it genuinely being in the raw lines — a plain scan for a line startingHost:is what actually works), and the scheme was hardcoded tohttp://regardless of the real request (is_https_ctx(), the same native builtinOAUTH2.DBA.check_https_ctxwraps, correctly reports which). This silently made every RSS/Atom<link>/<guid>relative instead of absolute — fixed as a side effect of building the permalink, not just for the permalink itself. A trailing CR/LF on the raw header line also survives plaintrim()(it only strips spaces) — stripchr(13)/chr(10)explicitly or every URL built fromsite_basegets a line break spliced into it.- Nested-VSP-string comment trap: a comment inside the deploy proc's embedded
index_contentstring that types out\r\nas literal backslash-r-backslash-n gets escape-interpreted into a REAL CR+LF at the outer deploy script's own compile time (confirmed live) — splitting that one comment across physical lines in the generatedindex.vsp, and the continuation line loses its--prefix, becoming uncommented (broken) VSP source. Never write literal\r/\nin a comment inside this string; describe the bytes in words instead. - Tagline link and help tooltip:
weblog_taglineis plain text, reused as-is in RSS<description>and<meta name="description">. The visible masthead span is built at deploy time as finished HTML ({{TAGLINE_HTML}}), escaped with plainreplace()so its raw UTF-8 bytes pass through — neversprintf('%V', ...)at request time, which re-encodes a narrow UTF-8 string as Latin-1 and garbles e.g. an em dash (the same double-encoding that garbled post titles), and which would escape an embedded<a>into literal text (confirmed live). To add a real link after the tagline (the pattern thedeploy-weblog-opl-site*.sqlsites use, e.g. "...a Weblog view of<a href="...">WebDAV Folder</a>"), passtagline_link_url/tagline_link_text(10th/11th). To give one phrase a help tooltip — the masthead guardrail's "Data Spaces" → "databases, knowledge bases, filesystems, and APIs" — passtagline_help_term/tagline_help_text(13th/14th); the first occurrence of the term is wrapped in<span class="term-help" title="...">. RSS and the meta description are unaffected by either. Each deploy records these on the collection, soWEBLOG_UPGRADEreuses them. - Migrating a site off a hand-built template (e.g. the openlinksw.com weblog, which ran
deploy-weblog-opl-site-facet.sql): the sameupgrade.sqlrun, withallow_template_overwriteset to 1 in its TARGET block. Its masthead (title, tagline, tooltip, link) is read from the old page, posts,schema:categoryfacets,schema:positionpins and the existing?post=/?q=/?category=/?feed=URLs carry over, and every later upgrade is an ordinary run with the opt-in back at 0. - Routes on a database-defined TLS listener must be SSL (blocking correctness requirement): at every startup Virtuoso's built-in
DB.DBA.VHOST_UPGRADElogs "Wrong record in virtual hosts config, operator attention required." for anyHTTP_PATHrow on:443that is notHP_SECURITY='SSL'or lackshttps_keyinHP_AUTH_OPTIONS. A:443listener set up in Conductor (www.openlinksw.com, UB) is such a listener, unlike the ini-defined*sslini*one (which the check skips -- why local testing missed it). The deploy now reads the listener's own TLS options (WEBLOG_LISTENER_TLS_OPTS) and gives every route it defines there -- public,-action,-admin--sec=>'SSL'plus those options;/weblog-actionhad beensec=>'digest'on www's:443(reported 2026-09-29).WEBLOG_LISTENER_TLS_OPTStreats a listener as TLS only whenSYS_HTTP_LISTENERS.HL_TYPE = 1(Virtuoso's own record; 0 = plain). Reading TLS options from anySSLroute was wrong: www's plain:80listener has a route markedSSLwith TLS options, so the deploy tried to make:80an SSL listener, failed withSR197partway (public route removed, admin route never defined ->/weblog-admin/404), 2026-09-29. Reproduced locally by editing a plain listener's route row toSSL+ key; fixed, then re-deployed cleanly.WEBLOG_ADMIN_AUTH_FNnow callsHTTP_AUTH_CHECK_USER (realm, 1, 1, null, 'digest')so admin logins stay Digest-only on an SSL-marked route (the scheme otherwise follows the route's security level, and SSL would also accept Basic). Verified locally on a temporary database-defined TLS listener: 0 rows the startup check would flag, public page 200, action route 401 Digest-only challenge, Basic rejected, Digest admin action and dashboard OK, plain-HTTP action route unchanged. - VAL sign-in for the dashboard and admin actions: when VAL (Virtuoso Authentication Layer) is installed, the admin gate uses it, so admins sign in with whatever VAL offers on that instance (SQL login, WebID-TLS / client certificate, OpenID Connect, OAuth, ...) and the weblog never handles a password itself. Pattern from the VAL VSP Authentication Tutorial.
WEBLOG_VAL_ADMIN_CHECK (coll)callsVAL.DBA.get_authentication_details_for_connectionand allows: a real SQL account thatWEBLOG_ADMIN_IS_OPERATORaccepts (dba,weblog:adminDavUser,WEBLOG_OPERATORmembers) or thatVAL.DBA.is_admin_usercounts as an instance admin; or any identity grantedoplacl:Writeonurn:virtuoso:access:weblog:{DAV collection}(WEBLOG_ADMIN_RESOURCE) by a VAL ACL rule in scopeurn:virtuoso:val:scopes:weblog(the deploy defines the scope, with no default access; checked withhonorScopeState=>0so a disabled scope can never fall back to default access). A WebID, an OIDC/OAuth account or a VAL group can therefore administer a weblog without a SQL account. The admin route now servesdashboard.vsp(owned bydav— DAV serves a.vspowned by another account as raw source), which callsWEBLOG_ADMIN_DASHBOARD_PAGE: signed in and allowed → the lockeddashboard.html, with a "Signed in as … · Sign out" bar; a browser not signed in, or signed in as someone not allowed → 302 to/val/authenticate.vsp(returnto,res,reslabel, anddeniedServiceIdfor "sign in as someone else"); a non-browser client or any request carrying anAuthorizationheader → HTTP Digest as before.WEBLOG_ADMIN_AUTH_FNon the action route checks VAL first (noAuthorizationheader), then Digest; a signed-in but unauthorized identity gets 403. Because a session cookie and a client certificate are both sent by the browser automatically, a VAL-authenticated action must carry the dashboard's form token (admin_token, stamped per identity byWEBLOG_ADMIN_CSRFfrom a registry-only secret), else 400; Digest-authenticated actions (scripts) don't need it. Without VAL everything is Digest-only, unchanged. Grant a WebID admin rights with (asdba):select VAL.DBA.acl_rule_new (serviceId=>VAL.DBA.user_personal_uri ('dba'), subject=>'urn:virtuoso:access:weblog:/DAV/…/', agent=>'{WebID}', access=>vector (VAL.DBA.oplacl_iri ('Write')), realm=>VAL.DBA.get_default_realm (), scope=>'urn:virtuoso:val:scopes:weblog', label=>'…');. WebID-TLS needs a TLS listener that requests a client certificate (https_verify3 in itsauth_opts, orX509ClientVerifyin the ini). Verified locally 2026-09-29: dashboard — anonymous browser → VAL login page, script → Digest 401, Digestdba200, VAL sessiondba200 with bar and stamped tokens, VAL session of a non-admin SQL user → login page withdeniedServiceId; WebID-TLS on a temporaryhttps_verify3 listener → refused (WebID named as denied) until an ACL rule granted it Write, then 200; action route — anonymous 401, DigestdbaOK without a token, VALdbaOK with its token, 400 without or with a wrong one, non-admin 403, WebID + token OK, WebID without token 400; public route still refuses admin actions;dashboard.htmlvia/DAVstill 401; fullupgrade.sqlon two local weblogs with 0 errors; non-dbaowner (kidehen) dashboard OK. VAL auto-creates its standard "Access to Profile Graph" rule for a WebID the first time it authenticates. - Route loops skip
/DAVrows with an emptyHP_LISTEN_HOST:VHOST_REMOVEcannot delete a row whoselhost/vhostare empty, so redefining a route there on redeploy always fails withSR197: Non unique primary key on DB.DBA.HTTP_PATH. - Subscriber email uniqueness:
WEBLOG_SUBSCRIBER_UQ, a unique index on(WS_DAV_COLLECTION, WS_EMAIL), is the actual duplicate guard — but it compares the literal stored string, so without canonicalizationAlice@x.comandalice@x.comwould pass it as two different rows for the same real address. Every write path (WEBLOG_NEWSLETTER_SUBSCRIBE,WEBLOG_NEWSLETTER_IMPORT_ONE, and the dashboard's manual-entry handler, which checks status directly rather than going throughIMPORT_ONE) lowercases the email before it is ever compared or stored, so the existing index enforces true case-insensitive uniqueness without a schema change. The manual-entry handler reports success only when every row it processed was actually added; any invalid address or already-confirmedduplicate is named individually in an error, never folded into a vague "N skipped" count. - Blocking correctness requirement: the
nl_action=confirmandnl_action=unsubscribelinks emailed to subscribers must never change subscription state on a plain GET request — only on POST. Mail-security gateways routinely prefetch every link found in an email with a GET to scan for phishing; if GET executed the action, that prefetch silently confirms or unsubscribes the real subscriber before they ever open the message (this happened live during development: addingList-Unsubscribecaused exactly this).index.vsp's newsletter dispatcher checks the raw request line fromhttp_request_header()[0](... like 'POST %') and, on GET, renders a confirmation page with a same-URL POST form instead of acting — a real POST (the human's click-through, or a mail client'sList-Unsubscribe-Postone-click) is what actually performs the change. Do not "simplify" this back to a single GET-triggered action. - Metadata source for categories: existing WebDAV custom properties, an analysis TSV, SQL staging rows, or generated suggestions requiring user approval.
- Pinning policy: pinning is part of the core engine. On deployment, if no non-zero
schema:positionexists in the weblog collection, the current first post by recency is seeded as the default pinned item. The pinning tool maintains one pinned post per collection by clearing siblingschema:positionvalues before setting the requested post. Manual metadata can still create multiple pinned posts, but the skill should treat that as drift to clean up.
Initial setup and upgrading an existing deployment
For deploy-weblog-skinned.sql deployments (the general-purpose, parameterized variant), both the FIRST deploy of a collection and an UPGRADE of one already running an older version of this skill's templates go through the same two files:
templates/register-weblog-newsletter.sql— newsletter tables/procedures.templates/deploy-weblog-skinned.sql— the engine procedures, includingDB.DBA.WEBLOG_DAV_DEPLOY_SKINNED, the actual deploy/redeploy call.
Both are idempotent installers: each DROPs then recreates its own procedures, so reinstalling either one is always safe to repeat. WEBLOG_DAV_DEPLOY_SKINNED itself is also safe to re-run against an already-deployed collection — it reads and preserves existing weblog:* collection properties (skin choice, newsletter config, admin email, etc.) and only refreshes index.vsp/dashboard.html, so redeploying an existing site picks up every template change (new admin panels, header/deliverability fixes, UI redesigns) with zero configuration loss. The one non-idempotent part is register-weblog-newsletter.sql's WEBLOG_SUBSCRIBER table (DROP+CREATE, wiping subscribers on every reinstall) — templates/upgrade.sql (below) backs this up automatically; running register-weblog-newsletter.sql directly, on its own, does not.
templates/upgrade.sql is a single-file convenience wrapper — a concatenation of both templates above, automatic pre-flight backups, and one upgrade call — for handing to an operator who applies it via their own isql session. It is the one file for both a fresh install and every upgrade. The only thing to edit is the TARGET block at the top: the weblog's DAV collection, the host of its public URL, and its path (plus, optionally, the masthead -- title, tagline, tagline link and help tooltip -- which a fresh install should fill in and an upgrade leaves blank to keep what the weblog has; a given value always wins, so the same fields also change an existing masthead; a collection that does not exist yet is created) (host and path separately, because a literal https://host/path in an isql-loaded file can trigger isql's macro substitution). Before Section 1 drops WEBLOG_SUBSCRIBER, its current contents (if any) are copied into a fresh, timestamped WEBLOG_SUBSCRIBER_BACKUP_<timestamp> table and restored right after; before the upgrade step overwrites index.vsp/dashboard.html, their current content is copied into DB.DBA.WEBLOG_UPGRADE_BACKUP. Neither backup table is ever dropped by a later run, and restore steps are documented in-line. This makes "can I revert if it fails" true by default for what this file changes — it is not a substitute for backing up anything else on the target instance.
Before the REDEPLOY block, TMP_WEBLOG_UPGRADE_GRANT_ROLE sets up a WEBLOG_OPERATOR SQL role and grants it EXECUTE on every DB.DBA.WEBLOG_* procedure this skill installs, then grants that role to the target weblog's own recorded admin user (weblog:adminDavUser; skipped for dba or on a first deploy) and to dba. dba is the SQL superuser, but DAV enforces the dashboard file's owner/group/world bits for whoever logs in over HTTP, dba included: with dashboard.html owned by another admin (UB: kidehen, 110110000N) dba got 403 Forbidden at /…-admin/ until it was a WEBLOG_OPERATOR member (verified locally 2026-09-28: 403 → 200 → 403 on grant/revoke). This exists because Virtuoso does not auto-grant execute on a new object to anyone but its owner/dba — verified live 2026-09-23 that a non-dba, non-owner session (a WebID-TLS connection mapped to a restricted account) gets SR186:SECURITY: No permission to execute procedure calling any of these by default. Idempotent and safe to re-run: the role is created once and reused, every matching procedure is re-granted on each run (so a procedure a future template change adds gets covered automatically), and GRANT <role> TO <user> — unlike GRANT EXECUTE ON <object> — is not idempotent on its own (a second grant to an existing member errors with U0013), so the script checks __SQL_MESSAGE for that specific case rather than treating any grant failure as fatal. Add another designated user with grant WEBLOG_OPERATOR to <username>;, run once as dba.
The file ends by calling DB.DBA.WEBLOG_UPGRADE (dav_collection, public_url, allow_template_overwrite := 0, dry_run := 0, admin_collection := null, weblog_title, weblog_tagline, tagline_link_url, tagline_link_text, tagline_help_term, tagline_help_text) (the last six optional; value sources in precedence order: given > recorded > current index.vsp > default) — the single upgrade entry point, also callable directly (select DB.DBA.WEBLOG_UPGRADE ('/DAV/demos/daas/', 'https://linkeddata.uriburner.com/weblog/');). It needs only the collection and public URL; everything else is looked up: route and host from the URL; title, tagline, tagline link and help tooltip, and admin host from the weblog:* properties every deploy now records on the collection (weblog:title, weblog:tagline, weblog:taglineLinkUrl/Text, weblog:taglineHelpTerm/Text, weblog:adminHost) — or, for a collection deployed before those were recorded or by a hand-built template, read from its current index.vsp by WEBLOG_UPGRADE_SCAN_INDEX (this template's <title><?= '…' ?>/<meta name="description">/tagline span, or a hand-built page's literal <title> and <span class="tagline"> including its term-help tooltip and trailing link); admin user, admin collection and skin from weblog:adminDavUser/weblog:adminCollection/weblog:skin, else dba, the deploy's default location, and classic. It returns a JSON report of every value used and where each came from (recorded / current index.vsp / default); dry_run => 1 returns that plan without changing anything. allow_template_overwrite => 1 is the one deliberate yes, needed only to replace an index.vsp this template did not generate — without it the call refuses with a message naming the fix. There is no per-site registry: a new site is upgraded the same way as an existing one. Rehearsed locally 2026-09-27: dry run, refusal, and migration of a hand-built openlinksw.com-style page (masthead, em dash, tooltip, link, posts, pin and facets all carried over; every value recorded on the next run), plus a no-change upgrade of an existing skinned weblog whose page was identical before and after.
This file is a dated snapshot, not a maintained source of truth: regenerate it (concatenate the two source templates plus the TARGET, backup, role-grant and upgrade blocks) whenever either source template changes materially, rather than trusting a stale copy indefinitely.
CAUTION when writing CREATE TABLE ... AS SELECT in Virtuoso SQL: verified live 2026-09-23 that it copies structure ONLY, not rows — a bare CTAS silently produces an empty "backup". Always follow it with an explicit INSERT ... SELECT to actually copy data, as templates/upgrade.sql's subscriber-backup step does. Also, a stored procedure body cannot directly INSERT/SELECT against a table that does not exist yet at compile time (Virtuoso resolves table references at procedure-compile time, not deferred to call time) — a table a procedure will write to must already exist before that CREATE PROCEDURE statement runs, even behind a runtime existence check.
When this skill is used to check or upgrade a remote, already-deployed collection, first fetch the public route read-only (HTTP GET, no credentials needed) to identify which template generation it's running — the presence/absence of class="theme-switch" vs the older class="theme-toggle", a /weblog-admin/ dashboard route, Subscribe/newsletter markup, facet support — before assuming credentials or a redeploy are needed.
Workflow
- Inspect the target collection or supplied SQL/VSP artifact before making assumptions. Exclude
._*,.DS_Store, hidden macOS sidecar files, and non-post assets from post enumeration. - Before authoring VSP or SQL, search prior memory/work for
DAV_RES_UPLOAD_STRSES_INT,string_output,HTTP_PATH,index.vsp,raw <?vsp,vsp_user, andweblog-from-webdav; reuse proven patterns and failure fixes. - Determine whether the weblog engine is already in place. Check that the target collection has an executable VSP entry point, working feed modes, collection-scoped search, date filtering, optional metadata facets, and the expected public route.
- If the engine is missing or incomplete, switch to
isql-enginesetup. Start from the templates intemplates/, patch paths, public URLs, title, and theme labels explicitly, then produce and, when credentials are available, deploy the complete VSP/isql bundle. - Validate the generated bundle with
scripts/validate_generated_weblog_bundle.py. This gate is blocking for WebDAV weblog generation. - Use WebDAV only for post publication after the engine check passes: copy HTML files, Markdown files, and associated asset folders. For normal post uploads, use
scripts/publish_with_metadata.pyso each uploaded document also receives verifiedschema:categorymetadata throughPROPPATCH. For out-of-band DAV uploads through mounted folders or third-party clients, installtemplates/register-category-refresh-scheduler.sqland scheduleDB.DBA.WEBLOG_DAV_REFRESH_CATEGORIESwith a default five-minute interval. - Use scoped search. In Virtuoso SQL, keep
contains/xcontainsas a top-levelANDpredicate and escape multi-word user input into a valid free-text expression. - Use calendar controls for date ranges in the sidebar, with
fromandtoparameters preserved in filtered links. - Show category facets only when
schema:categorymetadata exists. Facet counts must be computed from the filtered candidate set, not from all resources after a later display filter. - Support core and prompt-driven post pinning for a designated weblog. The engine deploy SQL seeds the newest valid post as the default pinned item only when no explicit pin exists. For user-requested changes, resolve the weblog target, verify the post resource exists, set
schema:positionto1for the designated post while clearing sibling pins, or reset it to0for unpinning, then verify the weblog order. - For agent-facing pinning, deploy
templates/register-weblog-pinning-tool.sqlthroughisql, registerDB.DBA.WEBLOG_DAV_SET_PIN, and verify it appears inOAI.DBA.LIST_CHAT_FUNCTIONS()and the generated OpenAPI description at/chat/functions/openapi.yaml. - Preserve the recency-ordered post list in the sidebar, with pinned posts promoted ahead of ordinary recency and the first pinned post selected as the default current post. Pinned posts must have an obvious but restrained visual indicator: a compact red-headed pin badge in the sidebar and a non-overlapping status strip above embedded posts or a compact kicker for non-embedded posts. Facets, explicit post selection, and search refine that list; they do not replace it with monthly buckets.
- Validate locally when possible, then verify the served route, feeds, search, date filters, facets, pinned ordering, registered OPAL tools, and a newly copied post.
Mode references
- For engine checks and SQL bootstrap, read
references/isql-mode.md. - For post publication by WebDAV copy, read
references/webdav-mode.md. - For template behavior and known VSP pitfalls, read
references/vsp-template-contract.md. - For custom metadata and category facets, read
references/facet-metadata-contract.md. - For OPAL/OpenAPI/MCP tool exposure, read
references/opal-tool-mode.md. - For the config-driven skin mechanism and how to add a new skin, read
references/skin-authoring-contract.md. - For initial setup or upgrading an existing
deploy-weblog-skinned.sqldeployment, see "Initial setup and upgrading an existing deployment" above andtemplates/upgrade.sql.
Practical guardrails
DAV_RES_UPLOAD_STRSES_INTneeds a string session object such asstring_output(), not a string literal passed to a reference parameter.- Avoid
$1,$2, and similar tokens in SQL loaded byisql; they can trigger macro substitution. Use named variables instead. - Do not rely on
SET MACRO_SUBSTITUTION OFFunless the target Virtuoso build accepts it; write scripts that avoid macro-looking text. - Cast WebDAV property values before
trim; Virtuoso can return integers for absent or encoded property states. - Use
dict_iter_nextfor category aggregate dictionaries; fragile key-list plus second lookup patterns can produce all-zero counts. - Keep feed links resolvable. POSH discovery and visible RSS/Atom buttons should point at actual feed variants handled by the VSP route.
- In the weblog masthead/footer, use the positioning line
Showcasing the power of loosely coupling Linked Data, AI Agents, Skills, and Data Spaces - a Weblog view of WebDAV Folder, withWebDAV Folderas the linked destination-generic anchor text. WrapData Spacesin a help affordance whose help text readsdatabases, knowledge bases, filesystems, and APIs. - Treat the certificate-bearing identity and the delegated principal as separate facts:
-Xor--certidentifies the calling agent, while-WorOn-Behalf-Ofidentifies the WebID principal whose ACL rights should be evaluated. - Weblog post iframes must tolerate opaque or sandboxed embedded content: frame helpers should catch access denial, skip unreadable frames, and route same-origin WebDAV collection links to top-level navigation when readable.
- Never write credentials into the skill, generated SQL, VSP, logs, or committed files.
- A
.vspresource only executes whenWS.WS.SYS_DAV_RES.RES_OWNERis thedavuser (uid 2); any other owner serves it as raw static source with no error. Always upload withDB.DBA.DAV_RES_UPLOAD_STRSES_INT(path, stream, mime, perms, 'dav', 'dav', ...)regardless of which account is deploying. Seeagent-rdf-memory/howto/osdi-vsp-execution-registration.ttl. - A VHOST route with
is_brws=>0anddef_pagedispatches every path under it to that default page, including a post's own filename — there is no way to reach an individual DAV resource directly through that route. Serve raw post content through a query param on the same route (?raw=<filename>, validated against an enumerated resource) instead of assuming a nested path or a second VHOST mapping will reach the file; a siblingis_brws=>1VHOST hit a 401 in testing and a nested path is captured by the parent route's own catch-all. - A POST to a nested
.vsppath under anis_brws=>0VHOST route gets redirected (trailing slash appended), which silently downgrades the request to GET and drops the form body. Route form submissions through the collection's existingindex.vspvia a query param instead of a dedicated endpoint file. - Remote uploads and removals use HTTP first, then WebDAV, then iSQL, with the principal PKCS#12 path and the
uriburner-p12Keychain item as the sole passphrase source (howto/remote-webdav-upload.ttl,howto/remote-resource-removal.ttl, prefs Steps 215–216). If Keychain access fails, stop and ask the user to resolve it; never use a temporary plaintext credential file. URIBurner operations use port 5443. Verify PUT and DELETE against the public URL and:5443before claiming success.
Safe Virtuoso ISQL Error Output
Whenever this workflow invokes OpenLink's native Virtuoso isql client, pass the bare client option COMMAND_TEXT_ON_ERROR=OFF, for example isql {host}:{port} {user} {password} COMMAND_TEXT_ON_ERROR=OFF {script.sql}. Place it before SQL input, script arguments, or -i. OFF maps to the client's internal value 0 and prevents the client from appending the full failed command to error output. The client accepts OFF/ON; literal 0 is rejected. Do not use -u for this setting (-u populates the U argument array). This does not redact secrets from server diagnostics, shell history, or process arguments, so keep credentials out of SQL text and logs. Do not apply this Virtuoso-specific option to unixODBC/iODBC isql clients.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

