<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>Markpin Blog</title>
  <link>https://markpin.dev/blog/</link>
  <description>Notes on reading on the web and on building Markpin.</description>
  <language>en</language>
  <atom:link href="https://markpin.dev/blog/feed.xml" rel="self" type="application/rss+xml"/>
  <item>
    <title>How to get web highlights into Obsidian, with a note on why you marked each one</title>
    <link>https://markpin.dev/blog/web-highlights-in-obsidian/</link>
    <guid isPermaLink="true">https://markpin.dev/blog/web-highlights-in-obsidian/</guid>
    <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
    <description>Highlight a web page in Chrome, add a mark and a short note, and get one Markdown file per page in your vault, with ==highlights==, callouts and properties Obsidian can read.</description>
    <content:encoded><![CDATA[<p>Obsidian's own Web Clipper is the best way I know to get a whole page into a vault. What I kept missing was the part that happens while I read: marking a line, saying why I marked it, and finding my place again the next day. I built Markpin for that part. It writes plain Markdown, so what it saves can live in the same vault. This post covers the setup and what ends up on disk.</p>
<h2>What you get</h2>
<p>One file per web page. Each highlight is written as a <code>==highlight==</code> with your note under it. The page's title, URL, tags and your one-line takeaway are properties.</p>
<figure><img src="https://markpin.dev/img/blog/obsidian-file.jpg" alt="A saved page opened in Obsidian, with properties on top and highlights and notes under Clips" loading="lazy"></figure>
<h2>Set it up</h2>
<ol><li>Install <a href="https://chromewebstore.google.com/detail/gapplocbnakkofbdbllljohlfceghgmb">Markpin from the Chrome Web Store</a>. It's free and there is no account.</li><li>Open the extension's options. Under <strong>Storage</strong>, pick <strong>Local folder</strong> and choose a folder inside your vault, for example <code>Vault/Markpin</code>. Chrome asks you to confirm access to the folder. After Chrome restarts, Markpin's options may say it needs the permission again. Click &quot;Allow again&quot;, choose &quot;Allow on every visit&quot;, and Chrome won't ask after that.</li><li>Under <strong>Markdown</strong>, pick the <strong>Obsidian</strong> template. It writes clips as <code>==highlights==</code> and <code>[!quote]</code> callouts. The default template writes blockquotes.</li></ol>
<p>The template applies to every file Markpin writes from then on. A page you saved earlier keeps its old shape until Markpin writes it again (a new highlight, note or tag), or until you run &quot;Rewrite existing files&quot; in the same options. Rewriting overwrites anything you changed by hand in Markpin's own properties or the body. Properties you added yourself are kept.</p>
<h2>Read and mark</h2>
<p>Select a sentence on any page. A small toolbar appears with two rows: <code>Clip ? ! × ★</code> on top, and a dot for each of the other highlight colors plus Bookmark below. One click on a color or a mark saves the highlight. If the page wasn't saved yet, that saves the page too.</p>
<p>The four marks are reactions:</p>
<ul><li><code>?</code> a question</li><li><code>!</code> something new to me</li><li><code>×</code> I disagree</li><li><code>★</code> the key point</li></ul>
<p>Right after the highlight is saved, a note box opens with a question already in it. The question depends on the mark: &quot;Why did you mark this?&quot; with no mark, &quot;What are you wondering?&quot; for <code>?</code>, &quot;What's new to you?&quot; for <code>!</code>, &quot;Why do you disagree?&quot; for <code>×</code> and &quot;Why does this matter most?&quot; for <code>★</code>. You can leave the box empty.</p>
<figure><img src="https://markpin.dev/img/blog/note-box-question.jpg" alt="The note box, opened after a highlight, asks a question that depends on the mark" loading="lazy"></figure>
<p>If you stop partway, leave a bookmark. A thin rail along the left edge of the page shows a dot for each highlight, a ribbon for the bookmark and a line for where you left off, and clicking a dot scrolls there. When you finish, click the bookmark and choose &quot;Mark as read&quot;. It asks for the page in one line, from memory. If you only skimmed it, &quot;Mark as skimmed&quot; in the same menu records that instead.</p>
<h2>What the file looks like</h2>
<p>Files land in <code>pages/&lt;year&gt;/</code> inside the folder you chose. Here is what one looks like, shortened and edited for the example.</p>
<pre><code>---
title: Pin
url: https://en.wikipedia.org/wiki/Pin
savedAt: 2026-10-01T14:46:40.646Z
tags:
  - pins
  - history
summary: Pins are fastening devices made from various materials.
note: Older than needles. Bone and thorn first, brass by the Bronze Age.
readAt: 2026-10-13T02:10:00.000Z
---

## Clips

==A pin is a device, typically pointed, used for fastening objects or fabrics together.==

compare with the safety pin page

==Many later pins were made of brass==

? why brass and not iron

&gt; [!quote]
&gt; Different types of pins

## Content

(the text of the article)
&lt;!-- markpin
{&quot;v&quot;:2,&quot;id&quot;:&quot;…&quot;,&quot;clips&quot;:[…]}
--&gt;</code></pre>
<ul><li><strong>Properties.</strong> <code>title</code>, <code>url</code>, <code>savedAt</code> and <code>tags</code> are always there. <code>summary</code> is there when one was written (Chrome's built-in AI writes it on devices that have the model). <code>note</code> is your one line. <code>readAt</code> or <code>skimmedAt</code> appears once you mark the page.</li><li><strong>Highlights.</strong> Each one is a <code>==highlight==</code>, which Obsidian renders as highlighted text.</li><li><strong>Your note.</strong> It sits on the line under the highlight. If you used a mark, the line starts with it, as in <code>? why brass and not iron</code>.</li><li><strong>Region clips.</strong> A paragraph or image you boxed on the page becomes a <code>[!quote]</code> callout.</li><li><strong>The last block.</strong> The comment at the end is the data Markpin needs to draw your highlights on the web page again. Reading view hides it. Live Preview shows it as gray text at the bottom of the note (one long line of JSON).</li></ul>
<h2>Working with the files in Obsidian</h2>
<p>They are ordinary notes, so search finds your notes and your one line along with the article text, and you can link to a page from anywhere with a normal link.</p>
<p>Two rules keep things from getting lost:</p>
<ol><li><strong>Add your own properties freely.</strong> Markpin keeps any property it doesn't recognize.</li><li><strong>Don't edit Markpin's own keys or the body by hand.</strong> The next time Markpin writes the file, those edits are overwritten. Write your longer thinking in your own note and link to the page.</li></ol>
<p>If you turn on &quot;Move deleted pages to the .trash/ folder&quot; in the options, a deleted page's file moves to <code>.trash/</code> inside the folder you chose (<code>Vault/Markpin/.trash/</code>, not the vault's own trash). It is off by default and works with folder storage only.</p>
<h2>Things to know before you rely on it</h2>
<ul><li>Markpin runs in Chrome on desktop (version 122 or later).</li><li>For a vault, use <strong>Local folder</strong>. The Google Drive option keeps its own Markpin folder in My Drive and does not write into a vault.</li><li>Highlight colors are not written to the body in the Obsidian template. They stay in the machine block and on the web page.</li><li>&quot;Where you left off&quot; is remembered on that computer only and is never written to the file.</li></ul>
<h2>Optional: let Claude read the same folder</h2>
<p>Markpin has a small MCP server that reads this folder, so an assistant such as Claude Code or Cursor can answer from what you saved. It runs on your computer, needs no API key and is read-only unless you add <code>--allow-write</code>. It needs Node.js 20.1 or newer.</p>
<pre><code>claude mcp add markpin -- npx -y @markpin/mcp@latest --dir ~/Vault/Markpin</code></pre>
<p>Then you can ask something like &quot;what did I highlight about brass pins?&quot; and it searches your pages.</p>]]></content:encoded>
  </item>
  <item>
    <title>I built a web highlighter that asks why you marked it, and saves the answer as Markdown</title>
    <link>https://markpin.dev/blog/why-did-you-mark-this/</link>
    <guid isPermaLink="true">https://markpin.dev/blog/why-did-you-mark-this/</guid>
    <pubDate>Tue, 13 Oct 2026 00:00:00 GMT</pubDate>
    <description>Seven decisions behind Markpin, a local-first Chrome extension: IndexedDB, one Markdown file per page, sync without a server, restoring highlights, and an MCP server.</description>
    <content:encoded><![CDATA[<p>Markpin is a web highlighter that asks why you marked the line. Select a sentence, pick a color or a mark, and a note box opens with a question already in it: &quot;Why did you mark this?&quot;</p>
<p>I've been a developer for eight years, and for most of that time I've read technical articles the same way: not right now, but save it somewhere first. &quot;Somewhere&quot; was Notion for a while, then Chrome bookmarks, and the links piled up in both. What never piled up was anything I had actually read. If I stopped halfway through an article, I couldn't find my spot again. The sentences I'd wanted to keep were gone. Thoughts I had while reading were too much hassle to write down separately, so I let them go. Eventually I built a Chrome extension that lets me do on the web what I do with a paper book: highlight a line, write a note in the margin, put a bookmark in, and open it again later. It's free and there's no account.</p>
<p>The question arrived in 1.2.0. Highlighting on its own rates low as a way to learn, while asking yourself &quot;why?&quot; and explaining things in your own words rate higher (Dunlosky et al., 2013). In Roediger and Karpicke's 2006 study, people who recalled a text from memory remembered more of it a week later than people who reread it, 61% to 40%. Those two results decided what the note box says and what happens when I mark a page as read.</p>
<p>The rest of this post is seven decisions I made while building the extension, in order.</p>
<figure><img src="https://markpin.dev/img/blog/note-box-question.jpg" alt="The note box, opened after a highlight, asks a question that depends on the mark" loading="lazy"></figure>
<h2>1. IndexedDB is the source of truth; chrome.storage holds settings only</h2>
<p>Saved pages and their clips (highlights, notes, region clips) live in the extension's IndexedDB. <code>chrome.storage</code> holds settings only: where to save, whether the floating button is shown, whether on-device AI is on. One reason is size: page bodies and clip images outgrow the default <code>chrome.storage.local</code> quota quickly (hence the <code>unlimitedStorage</code> permission). The other is transactions: a page and its clips are one record, so adding a clip is a read-modify-write that has to be atomic. The side panel's Zustand store holds UI state only and always re-reads data from IndexedDB. After the background worker writes, it broadcasts <code>STORE_CHANGED</code>; after the side panel writes, it re-reads on its own, because <code>sendMessage</code> doesn't echo back to the sender.</p>
<h2>2. Each page is one Markdown file, with machine data in a comment at the end</h2>
<p>I wanted the file to look clean in Obsidian and to round-trip through the extension without loss. The first version (v1) put every key in frontmatter, and Obsidian showed all of them in the Properties panel, with the nested <code>clips</code> key rendered as one line of JSON. So v2 splits it like this.</p>
<pre><code>---
title: Example article title
url: https://example.com/article
savedAt: 2026-09-16T10:00:00.000Z
tags:
  - react
  - state
summary: One-line summary
---

## Clips

&gt; Quoted sentence

My note

## Content

(body)
&lt;!-- markpin
{&quot;v&quot;:2,&quot;id&quot;:&quot;…&quot;,&quot;rev&quot;:3,&quot;updatedAt&quot;:&quot;…&quot;,&quot;domain&quot;:&quot;example.com&quot;,&quot;clips&quot;:[{&quot;id&quot;:&quot;…&quot;,&quot;type&quot;:&quot;text&quot;,&quot;createdAt&quot;:&quot;…&quot;,&quot;updatedAt&quot;:&quot;…&quot;,&quot;text&quot;:&quot;Quoted sentence&quot;,&quot;note&quot;:&quot;My note&quot;,&quot;selectors&quot;:[{&quot;type&quot;:&quot;TextQuoteSelector&quot;,&quot;exact&quot;:&quot;…&quot;,&quot;prefix&quot;:&quot;…&quot;,&quot;suffix&quot;:&quot;…&quot;},…],&quot;color&quot;:&quot;amber&quot;}]}
--&gt;</code></pre>
<p>Frontmatter keeps the human-readable keys: <code>title</code>, <code>url</code>, <code>savedAt</code> and <code>tags</code> are always there; <code>summary</code> (only when Chrome's built-in model produced one), <code>note</code>, <code>readAt</code>, <code>skimmedAt</code> and <code>deletedAt</code> appear when present. Machine data (<code>id rev updatedAt domain clips bookmark</code>) goes into one line of JSON inside <code>&lt;!-- markpin … --&gt;</code> at the end of the file; the excerpt above shortens it. The block sits at the end because Obsidian's Live Preview doesn't hide HTML comments; it shows them in gray (Reading view hides them). A <code>--&gt;</code> inside a string is written as <code>--\u003e</code> in that JSON line so it can't close the comment early, and <code>JSON.parse</code> restores it.</p>
<p>Unknown frontmatter keys and unknown block keys are both preserved, and a change that only adds keys doesn't bump <code>v</code>. A file is capped at 512 KB; if the body would overflow, it's truncated and <code>contentTruncated</code> is recorded. The parser still reads v1 (frontmatter <code>markpin: 1</code>). The &quot;rewrite existing files&quot; option in settings re-serializes everything to v2 with <code>rev</code> bumped but <code>updatedAt</code> untouched, so other devices don't produce conflict copies.</p>
<h2>3. Sync needs no server, only a folder or Google Drive</h2>
<p>There are three places to keep things: the browser only (the default), a folder on your machine (File System Access API, <code>showDirectoryPicker</code>), or Google Drive (with the <code>drive.file</code> scope, so the extension only sees files it created). The folder and Drive are two adapters behind one interface (<code>list</code>, <code>read</code>, <code>write</code>, <code>remove</code>), and one engine drives both.</p>
<p>The engine syncs 3 seconds after the last write and every 5 minutes via <code>chrome.alarms</code>. On failure it retries with exponential backoff: 1 minute, doubling, capped at 30 minutes. Conflicts are handled simply. A page with no sync history goes to the side with the later <code>updatedAt</code>; a page that has synced before and changed on both sides takes the copy already in the folder, and this device's version is kept as a <code>.conflict-</code> copy rather than deleted. It's crude, but for a personal tool with no server I decided it's enough. With folder storage there's also an option to move deleted pages to <code>.trash/</code>, following Obsidian's trash convention.</p>
<p>An MV3 service worker can die at any moment, so the periodic sync and the retry are both alarms, and what still needs pushing is tracked in IndexedDB. A sync requested while one is running sets an <code>again</code> flag and runs once more afterwards. While adapters are being switched, the old one is locked out. Service worker lifetime is the part of MV3 that bothers me most.</p>
<h2>4. Highlights are redrawn from stored selectors</h2>
<p>When you reopen a page, the highlight has to land on the same sentence. A text clip stores three W3C Web Annotation selectors: <code>TextQuoteSelector</code> (the exact text plus up to 32 characters of prefix and suffix), <code>TextPositionSelector</code> and <code>CssSelector</code>. A region clip stores a <code>CssSelector</code> and an <code>XPathSelector</code> per element. On restore, the quote is tried first against the whole page. If it misses, the CSS or XPath selector finds the element and the quote is retried inside it with whitespace normalized. As a last resort the normalized quote is tried against the whole page. The position selector is stored for interop with other tools; the matcher doesn't use it. A clip that matches nothing is marked as an orphan: it stays in the list but isn't drawn on the page.</p>
<p>Highlights are painted with the CSS Custom Highlight API, so the page's DOM isn't modified. The rest of the in-page UI (floating button, selection toolbar, note popover, rail) renders inside a Shadow DOM under <code>#markpin-host</code>, with no <code>!important</code> anywhere. The page body is captured only when you save, clip or bookmark.</p>
<h2>5. Summaries and tags come from Chrome's built-in model</h2>
<p>The one-line summary and tag suggestions use Chrome's built-in Prompt API (<code>LanguageModel</code>). The model runs on the device, so the page body never leaves it and there's no API key. When the UI language is en, ja, es, de or fr, the output language is declared through <code>expectedOutputs</code>; ko and pt-BR aren't accepted there yet, so for those the language is only set in the prompt. On a device without the model the feature stays off, and you can turn it off in settings too.</p>
<h2>6. Claude reads the folder over MCP, and writes need confirmation</h2>
<p><code>@markpin/mcp</code> is a stdio MCP server that runs over that folder (Node 20.1+). By default it exposes four read-only tools (<code>search get_page list_recent list_tags</code>); with <code>--allow-write</code> it registers twelve write tools (<code>add_clip set_clip_note set_bookmark update_tags …</code>). If the client supports elicitation, every write asks for confirmation first, because I didn't want an AI quietly editing my files. Every tool carries a <code>readOnlyHint</code>, and the write tools a <code>destructiveHint</code> as well. The server watches the folder, so a page you just saved in the browser is there for the next question. The server is MIT.</p>
<pre><code>claude mcp add markpin -- npx -y @markpin/mcp@latest --dir ~/Markpin --allow-write</code></pre>
<h2>7. 1.2.0 adds marks and a line from memory, and keeps reading position out of the file</h2>
<p>The selection toolbar now has two rows. The top row is <code>[Clip] ? ! × ★</code>. The bottom row has a dot for each of the other three highlight colors (Clip uses the current one), with Bookmark at the right end. One click on a color or a mark creates the clip. The marks stand for a question, something new, a disagreement and a key point. A clip has a color and at most one mark: color is for sorting, the mark is the reaction.</p>
<p>The note box that opens after a clip is saved now has a placeholder that depends on the mark: &quot;Why did you mark this?&quot; with no mark, &quot;What are you wondering?&quot; for <code>?</code>, &quot;What's new to you?&quot; for <code>!</code>, &quot;Why do you disagree?&quot; for <code>×</code>, and &quot;Why does this matter most?&quot; for <code>★</code>. You can leave the box empty.</p>
<p>&quot;Mark as read&quot; opens a second box, &quot;This page in one line&quot;, with the placeholder &quot;One line, from memory&quot; and a 500-character limit. The line shows under the title in the side panel and is searchable. There is also a third reading state next to bookmarked and read: skimmed. A page is in at most one of the three, and skimmed doesn't ask for a line.</p>
<p>All of this only adds keys, so the format is still v2. Here is the top of the same file after 1.2.0.</p>
<pre><code>---
title: Example article title
url: https://example.com/article
savedAt: 2026-09-16T10:00:00.000Z
tags:
  - react
  - state
summary: One-line summary
note: Most of what I kept in global state was server cache
readAt: 2026-10-12T08:30:00.000Z
---

## Clips

&gt; Quoted sentence

× only if every component reads it</code></pre>
<p>The mark is a prefix on the note line under the highlight, and the machine block carries it per clip as <code>&quot;kind&quot;:&quot;disagree&quot;</code> (the values are <code>question</code>, <code>insight</code>, <code>disagree</code> and <code>star</code>). The one line is the frontmatter property <code>note</code>. A skimmed page gets <code>skimmedAt</code> with an ISO time where this one has <code>readAt</code>. Empty values are omitted.</p>
<p>On the page, a 12 px rail along the left edge shows a dot per clip in its color, a ribbon for the bookmark and a gray line for where you left off. Clicking a dot scrolls there. The rail started on the right edge and moved left because it collided with macOS overlay scrollbars. Where you left off is recorded 1.5 seconds after scrolling stops. It lives in IndexedDB on that device and is never written to the Markdown file. Coming back triggers no toast or auto-scroll; the position only shows on the rail.</p>
<p>In the side panel, bookmarks older than 35 days get one card: &quot;Bookmarks over 35 days old: N · Still want to read them?&quot; Each page offers Keep, Mark as skimmed or Remove bookmark, and nothing is deleted.</p>
<p>In <code>@markpin/mcp</code> 0.9.0, <code>add_clip</code> accepts <code>kind</code>, and there are two new write tools: <code>set_note</code> (the one line; an empty string clears it, and it returns before and after) and <code>mark_skimmed</code>. Of the read tools, only <code>get_page</code> returns <code>kind</code>, <code>note</code> and <code>skimmedAt</code>; <code>search</code> and <code>list_recent</code> return summaries without them.</p>
<h2>Permissions and outbound requests</h2>
<p>Permissions are <code>storage unlimitedStorage sidePanel activeTab alarms identity</code>, plus the <code>&lt;all_urls&gt;</code> host permission; the content script runs on http and https pages. <code>identity</code> and <code>googleapis.com</code> are used only when you connect Drive. The extension makes no other outbound requests and has no analytics.</p>
<h2>Two questions for you</h2>
<p>On two of these decisions I'd rather hear from people who've used it than decide alone. There are no comments on this page, so write to support@markpin.dev.</p>
<ol><li>File format v2: human keys in frontmatter, machine data in a comment block at the end. If you use this in Obsidian or another Markdown tool, is there a point where the split gets in the way?</li><li>The next thing I'm looking at is coming back to what you've read. Highlight review, your notes in one place, or local reading stats: I'd like to hear which should come first.</li></ol>
<ul><li>Extension: <a href="https://chromewebstore.google.com/detail/gapplocbnakkofbdbllljohlfceghgmb">https://chromewebstore.google.com/detail/gapplocbnakkofbdbllljohlfceghgmb</a></li><li>MCP server: <a href="https://github.com/mud-studio/markpin-mcp">https://github.com/mud-studio/markpin-mcp</a></li></ul>]]></content:encoded>
  </item>
</channel>
</rss>
