skills/obsidian-web-clipper/SKILL.md
Author and debug Obsidian Web Clipper extension templates: template JSON, variables, filters, template logic, schema.org/CSS selectors, AI interpreter prompts, URL/schema triggers. Use when generating, importing, or fixing a clipper template, or matching one to a target site. Not for general scraping.
npx skillsauth add nweii/agent-stuff obsidian-web-clipperInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Obsidian Web Clipper is a browser extension that saves web content to an Obsidian vault as Markdown notes. Templates define how pages are captured — what metadata to extract, how to format the note, and which sites to auto-match.
Templates are configured as JSON. Users can import/export individual templates or full settings backups. When generating templates, output valid JSON the user can import directly.
Single-page scope. A clip only sees the page it fired on — the DOM loaded in the active tab. The extension is not an agent; it can't crawl, follow links, or visit other URLs. Selector, schema, and interpreter variables all resolve against that one page. If a template needs data spread across multiple pages, it can't gather it in one clip — design around what's on the page being clipped.
Caution: full settings exports contain interpreter API keys in plaintext (interpreter_settings.providers[].apiKey). Never quote them; flag exports stored in version control.
Templates hold opinionated conventions for folder layout, frontmatter property names, category/tag conventions, and note body structure. Match the user's existing conventions rather than inventing.
Before drafting a template, understand:
CLAUDE.md, AGENTS.md, READMEs)When conventions aren't discoverable or if the user isn't sure what they want, tailor what you can and fall back to minimal vendor-neutral choices. Flag any assumptions you've made so the user can adjust before import.
If the vault has a property sort order, mirror that order in the template's properties array.
{
"schemaVersion": "0.1.0",
"name": "Template Name",
"behavior": "create",
"noteNameFormat": "{{title}} - {{site}}",
"noteContentFormat": "{{content}}",
"vault": "VaultName",
"path": "folder/subfolder",
"context": "",
"properties": [],
"triggers": []
}
| Field | Type | Description |
| ------------------- | ------ | ----------------------------------------------------------- |
| schemaVersion | string | Always "0.1.0" |
| name | string | Display name shown in the extension |
| behavior | string | How the note is created (see below) |
| noteNameFormat | string | Filename for the note, supports variables |
| noteContentFormat | string | Note body content, supports variables |
| vault | string | Target vault name (optional — omit to use default) |
| path | string | Folder path within the vault (optional) |
| context | string | Limits what page content the interpreter AI sees (optional) |
| properties | array | Frontmatter properties (optional) |
| triggers | array | Auto-match rules for URLs/schemas (optional) |
When exporting a single template, the JSON is the template object directly (no wrapper). When part of a full settings export, templates are stored as template_[id] keys.
| Value | Description |
| ------------------ | ----------------------------------- |
| create | Create a new note |
| append-daily | Append to today's daily note |
| prepend-daily | Prepend to today's daily note |
| append-specific | Append to a specific existing note |
| prepend-specific | Prepend to a specific existing note |
All variables use {{variableName}} syntax. Filters chain with pipes: {{variable|filter1|filter2:"arg"}}. There are four kinds:
Preset variables — built-in page metadata: {{title}}, {{url}}, {{author}}, {{site}}, {{published}}, {{description}}, {{content}} (full article markdown), {{date}}, {{highlights}}, etc. Look up the full list in the live docs.
Schema variables — extract Schema.org JSON-LD from the page. Patterns:
{{schema:name}} — first match anywhere
{{schema:@Recipe:name}} — scoped to a specific @type
{{schema:author.name}} — nested keys via dots
{{schema:image[0].contentUrl}} — array index
{{schema:actors[*].name}} — flatten arrays
Schema-driven templates are usually the right choice for domain-specific clippers (recipes, films, books, jobs) since one schema:@Type trigger matches across many sites.
Selector variables — pull content via CSS selectors when there's no schema:
{{selector:h1}} — text content
{{selector:img.hero?src}} — attribute value
{{selectorHtml:article}} — raw HTML
{{selectorHtml:body|markdown}} — HTML → Markdown
Interpreter variables — natural-language prompts evaluated by an LLM (requires the user has an LLM provider configured in extension settings):
{{"a summary of the page"}}
{{"3 tags describing this content"}}
{{"return JSON array with fields: author, text"|map:item => item.text|join:"\n"}}
The context field at the template top level controls what page content the AI sees. Use {{selectorHtml:#main}} or wrap structured context like <page>\nTitle: {{title}}\n{{content}}\n</page> to keep prompts focused.
Chain filters with |. Look up specific filter signatures in the live docs (see Reference at the bottom). The patterns and gotchas below are the operationally non-obvious parts.
Minutiae that might trip up template authors:
String concatenation with + doesn't work inside map callbacks. The expression parser treats + as an unexpected character and fails the template import with "Unexpected character '+' in template". Use template literals instead:
✗ |map:item => "- " + item (parse error on import)
✓ |map:item => "- ${item}" (template literal — correct)
Built-in filters cannot be chained inside map. Flatten or transform inside map, then apply filters to the result outside:
✗ |map:item => item.text|trim (filter not allowed inside map)
✓ |map:item => item.text|join:"\n"|trim (filter chain outside map)
For arrays of objects with multi-line per-item output, use map to flatten to flat keys, then template. Nested key access in template literals is not documented and unreliable; flattening first is the safe pattern:
|map:item => ({name: item.author.name, body: item.reviewBody})
|template:"${name}:\n> ${body}\n\n"
duration outputs HH:mm:ss, not compact human format. For something like 45m or 1h 30m, skip the filter and chain replace on the raw ISO 8601 string:
{{schema:@Recipe:prepTime|replace:"PT":""|replace:"H":"h "|replace:"M":"m"|trim}}
→ "PT1H30M" becomes "1h 30m"
→ "PT45M" becomes "45m"
|list works on string arrays directly — no need to map a "- " + item prefix yourself. Use |list for bullets, |list:numbered for numbered, |list:task for checkboxes.
{{url|split:"?"|slice:0,1}} — strip query params
{{schema:actors[*].name|wikilink|slice:0,4|join}} — first 4 actors as wikilinks
{{highlights|map:item => item.text|join:"\n\n"|blockquote}} — highlights as blockquote
{{date|date:"YYYY-MM-DD-ddd"}} — formatted date with day name
{{"return JSON..."|map:item => item.title|join:"\n"}} — AI → structured → formatted
Templates support Twig/Liquid-style logic in noteContentFormat, noteNameFormat, and property values: {% if %}/{% elseif %}/{% else %} conditionals, {% for %} loops (with a loop object: loop.index, loop.first, loop.last, etc.), {% set %} variable assignment, and ?? fallbacks ({{title ?? "Untitled"}}). Full syntax lives in Logic.md in the live docs (see Reference). The non-obvious parts:
?? — {{title|upper ?? "X"}} applies upper before the fallback check; parenthesize if the fallback needs the filter instead.{% set %} accepts selector results ({% set comments = selector:.comment %}), which pairs with bracket indexing ({{timestamps[loop.index0]}}) to walk two parallel arrays in one loop.Properties become Obsidian frontmatter fields. Order matters: the extension writes properties to frontmatter in the order they appear in the array. If the target vault uses a YAML sort plugin (Linter, etc.), match the template's property order to the user's sort order so freshly clipped notes don't reshuffle on lint.
Each property in the array:
{
"name": "property_name",
"value": "{{variable|filter}}",
"type": "text"
}
| Type | Description |
| ----------- | ------------------------------------------------ |
| text | Single string value |
| multitext | Array (comma-separated values become list items) |
| date | Date value (ISO format or use date filter) |
| number | Numeric value |
| checkbox | Boolean — expects "true" or "false" |
Interpreter prompts inside JSON property value strings require escaped quotes. The inner " that delimit the prompt must be escaped as \\\" in the JSON:
{
"name": "description",
"value": "{{\\\"Summarize in 1-2 sentences\\\"}}",
"type": "text"
}
The escaping layers:
" → JSON string delimiter\\\" → produces literal \" in the parsed string{{"Summarize in 1-2 sentences"}} and sends to interpreterStatic values and preset variables don't need this escaping — only interpreter prompts inside JSON strings.
Filters on interpreter prompts in properties also need escaping:
"value": "{{\\\"author's full name\\\"|wikilink}}"
Auto-select this template when the URL or page schema matches.
| Type | Format | Example |
| ----------- | ---------------- | ----------------------------------------------- |
| URL prefix | Plain URL string | "https://letterboxd.com/film/" |
| Regex | Enclosed in / | "/^https?:\\/\\/docs\\.google\\.com\\/forms/" |
| Schema type | schema:@Type | "schema:@NewsArticle" |
Multiple triggers in the array act as OR — any match selects the template.
A vendor-neutral article clipper. Captures the full article body and a small set of metadata; mixes preset variables with optional AI-interpreter prompts. Treat the folder, tag, and prompt choices as illustrative — substitute the user's own conventions before delivering.
{
"schemaVersion": "0.1.0",
"name": "Article",
"behavior": "create",
"noteNameFormat": "{{title}} - {{site}}",
"noteContentFormat": "{{content}}",
"path": "Clippings",
"properties": [
{
"name": "title",
"value": "{{title}}",
"type": "text"
},
{
"name": "url",
"value": "{{url|split:\\\"?\\\"|slice:0,1}}",
"type": "text"
},
{
"name": "author",
"value": "{{author}}",
"type": "text"
},
{
"name": "site",
"value": "{{site}}",
"type": "text"
},
{
"name": "published",
"value": "{{published}}",
"type": "date"
},
{
"name": "description",
"value": "{{description}}",
"type": "text"
},
{
"name": "tags",
"value": "{{\\\"2-3 comma-separated topical tags, lowercase, max two words each\\\"}}",
"type": "multitext"
},
{
"name": "created",
"value": "{{date}}",
"type": "date"
}
],
"triggers": []
}
Key patterns in this example:
noteContentFormat: "{{content}}" captures the full article body in Markdown.noteNameFormat combines page title with site name; swap to whatever filename shape the user prefers.{{url|split:\\\"?\\\"|slice:0,1}}.description uses the page's own description ({{description}}); replace with an interpreter prompt only if the user wants AI-generated summaries.tags uses an AI-interpreter prompt for topical tags. Drop or replace with a static value if the user doesn't have the interpreter enabled.For domain-specific templates (recipes, films, books, jobs, etc.), prefer schema-driven extraction ({{schema:@Type:key}}) and schema:@Type triggers — schema-based templates auto-match across many sites of the same conceptual domain.
To convert an existing AI prompt into a web clipper interpreter variable:
{{"..."}} for use in noteContentFormat or noteNameFormatvalue, escape as {{\\\"...\\\"}}{{"prompt"|filter1|filter2}}map and template filterscontext provides)Template parse errors are silent until the user tries to import — and the import error is often laconic ("Unexpected character 'X' in template" with a line number pointing at the JSON, not the template body). Before declaring a template done:
map, template, and replace chain for the gotchas above.For exhaustive filter signatures, the full preset variable list, schema variable edge cases, and anything not covered above, fetch the live docs. Two source-of-truth locations:
docs/ (e.g. obsidianmd/obsidian-clipper, look for docs/Filters.md, docs/Variables.md, docs/Templates.md, docs/Logic.md, docs/Interpret web pages.md)obsidianmd/obsidian-help)Canonical human-facing docs: https://obsidian.md/help/web-clipper (subpages /filters, /logic, etc.). These are JS-rendered; a plain fetch returns an empty shell. Link users there, but read from GitHub.
Use whatever retrieval tool fits — context7, WebFetch, direct GitHub raw URLs, or anything else. Pick the one that's faster or more accurate in the moment. Prefer the live docs over guessing from memory.
testing
Command-invoked tutoring pass for understanding something deeply: a concept being learned, or work just done in the session. Locates where the learner is, teaches one step per turn, quizzes to verify, and continues until they can explain the material back and apply it. Can produce durable artifacts (a walkthrough of the work, a record of what was learned, a glossary) saved through whatever the environment supports. Best run after substantive work, or when the aim is to learn something.
testing
Search, read, filter, combine, adapt, and save recipes in the Brain vault collection. Use whenever cooking and the collection are relevant — 'what should I make', 'recipes with miso', 'save this one' all imply it.
testing
Socratic teaching pass over the work just done in a session: incremental comprehension stages, a running checklist doc, restate-understanding-first, and AskUserQuestion quizzes. The session doesn't end until the user has demonstrated understanding. Run after Claude has completed substantive work worth deeply understanding.
development
Writing-partner processes that draw out the user's own writing through questioning: guided drafting sessions, fragment mining, shaping raw material into a piece, and phrase tightening. Use for help discovering, developing, and structuring writing (notes, essays, messages, etc).