Skip to content

Instantly share code, notes, and snippets.

@kepano
Last active May 19, 2024 06:12
Show Gist options
  • Save kepano/90c05f162c37cf730abb8ff027987ca3 to your computer and use it in GitHub Desktop.
Save kepano/90c05f162c37cf730abb8ff027987ca3 to your computer and use it in GitHub Desktop.
Obsidian Web Clipper Bookmarklet to save articles and pages from the web (for Safari, Chrome, Firefox, and mobile browsers)

By @kepano

Visit my website to see a demo and find more information about this project.

Installation

Create a new bookmark in your browser, then copy/paste the minified code below into the URL field.

You can customize the output using the optional variables at the top, and the template at the bottom. The default template is designed for use with the Dataview plugin. If you make changes I recommend using Bookmarklet Maker to minify and URI encode the bookmarklet.

Usage

By default, clicking the bookmarklet creates a new Obsidian file from the main body of the article (similar to Readability view). Alternatively you can choose to create a file from a selection, by either selecting all (CMD+A), or just a portion of the page.

Any images in the content will be embedded as external references. If you want to download images locally you can use the Local Images plugin which allows you to download images for a note.

Troubleshooting

This bookmarklet may not work on all websites. If you run into issues, you can also try the MarkDownload browser extension which provides similar functionality. You can troubleshoot issues by opening the Developer Console in your browser and checking if any errors appear when you click the bookmarklet. The most common error is that a website or the browser itself is blocking third party code execution. Unfortunately there is no good solve for that yet.

javascript: Promise.all([import('https://unpkg.com/turndown@6.0.0?module'), import('https://unpkg.com/@tehshrike/readability@0.2.0'), ]).then(async ([{
default: Turndown
}, {
default: Readability
}]) => {
/* Optional vault name */
const vault = "";
/* Optional folder name such as "Clippings/" */
const folder = "Clippings/";
/* Optional tags */
let tags = "clippings";
/* Parse the site's meta keywords content into tags, if present */
if (document.querySelector('meta[name="keywords" i]')) {
var keywords = document.querySelector('meta[name="keywords" i]').getAttribute('content').split(',');
keywords.forEach(function(keyword) {
let tag = ' ' + keyword.split(' ').join('');
tags += tag;
});
}
function getSelectionHtml() {
var html = "";
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
return html;
}
const selection = getSelectionHtml();
const {
title,
byline,
content
} = new Readability(document.cloneNode(true)).parse();
function getFileName(fileName) {
var userAgent = window.navigator.userAgent,
platform = window.navigator.platform,
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
if (windowsPlatforms.indexOf(platform) !== -1) {
fileName = fileName.replace(':', '').replace(/[/\\?%*|"<>]/g, '-');
} else {
fileName = fileName.replace(':', '').replace(/\//g, '-').replace(/\\/g, '-');
}
return fileName;
}
const fileName = getFileName(title);
if (selection) {
var markdownify = selection;
} else {
var markdownify = content;
}
if (vault) {
var vaultName = '&vault=' + encodeURIComponent(`${vault}`);
} else {
var vaultName = '';
}
const markdownBody = new Turndown({
headingStyle: 'atx',
hr: '---',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
emDelimiter: '*',
}).turndown(markdownify);
var date = new Date();
function convertDate(date) {
var yyyy = date.getFullYear().toString();
var mm = (date.getMonth()+1).toString();
var dd = date.getDate().toString();
var mmChars = mm.split('');
var ddChars = dd.split('');
return yyyy + '-' + (mmChars[1]?mm:"0"+mmChars[0]) + '-' + (ddChars[1]?dd:"0"+ddChars[0]);
}
const today = convertDate(date);
// Utility function to get meta content by name or property
function getMetaContent(attr, value) {
var element = document.querySelector(`meta[${attr}='${value}']`);
return element ? element.getAttribute("content").trim() : "";
}
// Fetch byline, meta author, property author, or site name
var author = byline || getMetaContent("name", "author") || getMetaContent("property", "author") || getMetaContent("property", "og:site_name");
// Check if there's an author and add brackets
var authorBrackets = author ? `"[[${author}]]"` : "";
/* Try to get published date */
var timeElement = document.querySelector("time");
var publishedDate = timeElement ? timeElement.getAttribute("datetime") : "";
if (publishedDate && publishedDate.trim() !== "") {
var date = new Date(publishedDate);
var year = date.getFullYear();
var month = date.getMonth() + 1; // Months are 0-based in JavaScript
var day = date.getDate();
// Pad month and day with leading zeros if necessary
month = month < 10 ? '0' + month : month;
day = day < 10 ? '0' + day : day;
var published = year + '-' + month + '-' + day;
} else {
var published = ''
}
/* YAML front matter as tags render cleaner with special chars */
const fileContent =
'---\n'
+ 'category: "[[Clippings]]"\n'
+ 'author: ' + authorBrackets + '\n'
+ 'title: "' + title + '"\n'
+ 'source: ' + document.URL + '\n'
+ 'clipped: ' + today + '\n'
+ 'published: ' + published + '\n'
+ 'topics: \n'
+ 'tags: [' + tags + ']\n'
+ '---\n\n'
+ markdownBody ;
document.location.href = "obsidian://new?"
+ "file=" + encodeURIComponent(folder + fileName)
+ "&content=" + encodeURIComponent(fileContent)
+ vaultName ;
})
javascript:(function()%7Bjavascript%3A%20Promise.all(%5Bimport('https%3A%2F%2Funpkg.com%2Fturndown%406.0.0%3Fmodule')%2C%20import('https%3A%2F%2Funpkg.com%2F%40tehshrike%2Freadability%400.2.0')%2C%20%5D).then(async%20(%5B%7B%0A%20%20%20%20default%3A%20Turndown%0A%7D%2C%20%7B%0A%20%20%20%20default%3A%20Readability%0A%7D%5D)%20%3D%3E%20%7B%0A%0A%20%20%2F*%20Optional%20vault%20name%20*%2F%0A%20%20const%20vault%20%3D%20%22%22%3B%0A%0A%20%20%2F*%20Optional%20folder%20name%20such%20as%20%22Clippings%2F%22%20*%2F%0A%20%20const%20folder%20%3D%20%22Clippings%2F%22%3B%0A%0A%20%20%2F*%20Optional%20tags%20%20*%2F%0A%20%20let%20tags%20%3D%20%22clippings%22%3B%0A%0A%20%20%2F*%20Parse%20the%20site's%20meta%20keywords%20content%20into%20tags%2C%20if%20present%20*%2F%0A%20%20if%20(document.querySelector('meta%5Bname%3D%22keywords%22%20i%5D'))%20%7B%0A%20%20%20%20%20%20var%20keywords%20%3D%20document.querySelector('meta%5Bname%3D%22keywords%22%20i%5D').getAttribute('content').split('%2C')%3B%0A%0A%20%20%20%20%20%20keywords.forEach(function(keyword)%20%7B%0A%20%20%20%20%20%20%20%20%20%20let%20tag%20%3D%20'%20'%20%2B%20keyword.split('%20').join('')%3B%0A%20%20%20%20%20%20%20%20%20%20tags%20%2B%3D%20tag%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%7D%0A%0A%20%20function%20getSelectionHtml()%20%7B%0A%20%20%20%20var%20html%20%3D%20%22%22%3B%0A%20%20%20%20if%20(typeof%20window.getSelection%20!%3D%20%22undefined%22)%20%7B%0A%20%20%20%20%20%20%20%20var%20sel%20%3D%20window.getSelection()%3B%0A%20%20%20%20%20%20%20%20if%20(sel.rangeCount)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20var%20container%20%3D%20document.createElement(%22div%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(var%20i%20%3D%200%2C%20len%20%3D%20sel.rangeCount%3B%20i%20%3C%20len%3B%20%2B%2Bi)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20container.appendChild(sel.getRangeAt(i).cloneContents())%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20html%20%3D%20container.innerHTML%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20else%20if%20(typeof%20document.selection%20!%3D%20%22undefined%22)%20%7B%0A%20%20%20%20%20%20%20%20if%20(document.selection.type%20%3D%3D%20%22Text%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20html%20%3D%20document.selection.createRange().htmlText%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20return%20html%3B%0A%20%20%7D%0A%0A%20%20const%20selection%20%3D%20getSelectionHtml()%3B%0A%0A%20%20const%20%7B%0A%20%20%20%20%20%20title%2C%0A%20%20%20%20%20%20byline%2C%0A%20%20%20%20%20%20content%0A%20%20%7D%20%3D%20new%20Readability(document.cloneNode(true)).parse()%3B%0A%0A%20%20function%20getFileName(fileName)%20%7B%0A%20%20%20%20var%20userAgent%20%3D%20window.navigator.userAgent%2C%0A%20%20%20%20%20%20%20%20platform%20%3D%20window.navigator.platform%2C%0A%20%20%20%20%20%20%20%20windowsPlatforms%20%3D%20%5B'Win32'%2C%20'Win64'%2C%20'Windows'%2C%20'WinCE'%5D%3B%0A%0A%20%20%20%20if%20(windowsPlatforms.indexOf(platform)%20!%3D%3D%20-1)%20%7B%0A%20%20%20%20%20%20fileName%20%3D%20fileName.replace('%3A'%2C%20'').replace(%2F%5B%2F%5C%5C%3F%25*%7C%22%3C%3E%5D%2Fg%2C%20'-')%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20fileName%20%3D%20fileName.replace('%3A'%2C%20'').replace(%2F%5C%2F%2Fg%2C%20'-').replace(%2F%5C%5C%2Fg%2C%20'-')%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20fileName%3B%0A%20%20%7D%0A%20%20const%20fileName%20%3D%20getFileName(title)%3B%0A%0A%20%20if%20(selection)%20%7B%0A%20%20%20%20%20%20var%20markdownify%20%3D%20selection%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20var%20markdownify%20%3D%20content%3B%0A%20%20%7D%0A%0A%20%20if%20(vault)%20%7B%0A%20%20%20%20%20%20var%20vaultName%20%3D%20'%26vault%3D'%20%2B%20encodeURIComponent(%60%24%7Bvault%7D%60)%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20var%20vaultName%20%3D%20''%3B%0A%20%20%7D%0A%0A%20%20const%20markdownBody%20%3D%20new%20Turndown(%7B%0A%20%20%20%20%20%20headingStyle%3A%20'atx'%2C%0A%20%20%20%20%20%20hr%3A%20'---'%2C%0A%20%20%20%20%20%20bulletListMarker%3A%20'-'%2C%0A%20%20%20%20%20%20codeBlockStyle%3A%20'fenced'%2C%0A%20%20%20%20%20%20emDelimiter%3A%20'*'%2C%0A%20%20%7D).turndown(markdownify)%3B%0A%0A%20%20var%20date%20%3D%20new%20Date()%3B%0A%0A%20%20function%20convertDate(date)%20%7B%0A%20%20%20%20var%20yyyy%20%3D%20date.getFullYear().toString()%3B%0A%20%20%20%20var%20mm%20%3D%20(date.getMonth()%2B1).toString()%3B%0A%20%20%20%20var%20dd%20%20%3D%20date.getDate().toString()%3B%0A%20%20%20%20var%20mmChars%20%3D%20mm.split('')%3B%0A%20%20%20%20var%20ddChars%20%3D%20dd.split('')%3B%0A%20%20%20%20return%20yyyy%20%2B%20'-'%20%2B%20(mmChars%5B1%5D%3Fmm%3A%220%22%2BmmChars%5B0%5D)%20%2B%20'-'%20%2B%20(ddChars%5B1%5D%3Fdd%3A%220%22%2BddChars%5B0%5D)%3B%0A%20%20%7D%0A%0A%20%20const%20today%20%3D%20convertDate(date)%3B%0A%0A%20%20%2F%2F%20Utility%20function%20to%20get%20meta%20content%20by%20name%20or%20property%0A%20%20function%20getMetaContent(attr%2C%20value)%20%7B%0A%20%20%20%20%20%20var%20element%20%3D%20document.querySelector(%60meta%5B%24%7Battr%7D%3D'%24%7Bvalue%7D'%5D%60)%3B%0A%20%20%20%20%20%20return%20element%20%3F%20element.getAttribute(%22content%22).trim()%20%3A%20%22%22%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20Fetch%20byline%2C%20meta%20author%2C%20property%20author%2C%20or%20site%20name%0A%20%20var%20author%20%3D%20byline%20%7C%7C%20getMetaContent(%22name%22%2C%20%22author%22)%20%7C%7C%20getMetaContent(%22property%22%2C%20%22author%22)%20%7C%7C%20getMetaContent(%22property%22%2C%20%22og%3Asite_name%22)%3B%0A%0A%20%20%2F%2F%20Check%20if%20there's%20an%20author%20and%20add%20brackets%0A%20%20var%20authorBrackets%20%3D%20author%20%3F%20%60%22%5B%5B%24%7Bauthor%7D%5D%5D%22%60%20%3A%20%22%22%3B%0A%0A%0A%20%20%2F*%20Try%20to%20get%20published%20date%20*%2F%0A%20%20var%20timeElement%20%3D%20document.querySelector(%22time%22)%3B%0A%20%20var%20publishedDate%20%3D%20timeElement%20%3F%20timeElement.getAttribute(%22datetime%22)%20%3A%20%22%22%3B%0A%0A%20%20if%20(publishedDate%20%26%26%20publishedDate.trim()%20!%3D%3D%20%22%22)%20%7B%0A%20%20%20%20%20%20var%20date%20%3D%20new%20Date(publishedDate)%3B%0A%20%20%20%20%20%20var%20year%20%3D%20date.getFullYear()%3B%0A%20%20%20%20%20%20var%20month%20%3D%20date.getMonth()%20%2B%201%3B%20%2F%2F%20Months%20are%200-based%20in%20JavaScript%0A%20%20%20%20%20%20var%20day%20%3D%20date.getDate()%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Pad%20month%20and%20day%20with%20leading%20zeros%20if%20necessary%0A%20%20%20%20%20%20month%20%3D%20month%20%3C%2010%20%3F%20'0'%20%2B%20month%20%3A%20month%3B%0A%20%20%20%20%20%20day%20%3D%20day%20%3C%2010%20%3F%20'0'%20%2B%20day%20%3A%20day%3B%0A%0A%20%20%20%20%20%20var%20published%20%3D%20year%20%2B%20'-'%20%2B%20month%20%2B%20'-'%20%2B%20day%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20var%20published%20%3D%20''%0A%20%20%7D%0A%0A%20%20%2F*%20YAML%20front%20matter%20as%20tags%20render%20cleaner%20with%20special%20chars%20%20*%2F%0A%20%20const%20fileContent%20%3D%20%0A%20%20%20%20%20%20'---%5Cn'%0A%20%20%20%20%20%20%2B%20'category%3A%20%22%5B%5BClippings%5D%5D%22%5Cn'%0A%20%20%20%20%20%20%2B%20'author%3A%20'%20%2B%20authorBrackets%20%2B%20'%5Cn'%0A%20%20%20%20%20%20%2B%20'title%3A%20%22'%20%2B%20title%20%2B%20'%22%5Cn'%0A%20%20%20%20%20%20%2B%20'source%3A%20'%20%2B%20document.URL%20%2B%20'%5Cn'%0A%20%20%20%20%20%20%2B%20'clipped%3A%20'%20%2B%20today%20%2B%20'%5Cn'%0A%20%20%20%20%20%20%2B%20'published%3A%20'%20%2B%20published%20%2B%20'%5Cn'%20%0A%20%20%20%20%20%20%2B%20'topics%3A%20%5Cn'%0A%20%20%20%20%20%20%2B%20'tags%3A%20%5B'%20%2B%20tags%20%2B%20'%5D%5Cn'%0A%20%20%20%20%20%20%2B%20'---%5Cn%5Cn'%0A%20%20%20%20%20%20%2B%20markdownBody%20%3B%0A%0A%20%20%20document.location.href%20%3D%20%22obsidian%3A%2F%2Fnew%3F%22%0A%20%20%20%20%2B%20%22file%3D%22%20%2B%20encodeURIComponent(folder%20%2B%20fileName)%0A%20%20%20%20%2B%20%22%26content%3D%22%20%2B%20encodeURIComponent(fileContent)%0A%20%20%20%20%2B%20vaultName%20%3B%0A%0A%7D)%7D)()%3B
@ganesshkumar
Copy link

@kepano, I have been using this and I have couple of bookmarklets to clip the contents into different folders in my vault. Since I created bookmarklets for various folders, I created a simple UI to fill in the vault name, folder name, tags and get the bookmarklet.

http://obsidian-clipper-maker.ganesshkumar.com

Let me know if this is useful for you guys!

@jordanwiseman
Copy link

jordanwiseman commented Mar 5, 2022

Hey, @kepano, fantastic work! FYI, I forked a version and added auto-tagging based on a site's <meta name="keywords" .... tag, and using YAML front matter (a la @Moonbase59) as it seems to handle less predictable alphanumeric-only tags better.

Code:

javascript: Promise.all([import('https://unpkg.com/turndown@6.0.0?module'), import('https://unpkg.com/@tehshrike/readability@0.2.0'), ]).then(async ([{
    default: Turndown
}, {
    default: Readability
}]) => {

  /* Optional vault name */
  const vault = "";

  /* Optional folder name such as "Clippings/" */
  const folder = "";

  /* Optional tags  */
  let tags = "clippings";

  /* parse and lightly clean the site's meta keywords content into tags, if present */
  if (document.querySelector('meta[name="keywords" i]')) {
      var keywords = document.querySelector('meta[name="keywords" i]').getAttribute('content').split(',');

      keywords.forEach(function(keyword) {
          let tag = ' ' + keyword.split(' ').join('');
          tags += tag;
      });
  }

  function getSelectionHtml() {
    var html = "";
    if (typeof window.getSelection != "undefined") {
        var sel = window.getSelection();
        if (sel.rangeCount) {
            var container = document.createElement("div");
            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                container.appendChild(sel.getRangeAt(i).cloneContents());
            }
            html = container.innerHTML;
        }
    } else if (typeof document.selection != "undefined") {
        if (document.selection.type == "Text") {
            html = document.selection.createRange().htmlText;
        }
    }
    return html;
  }

  const selection = getSelectionHtml();

  const {
      title,
      byline,
      content
  } = new Readability(document.cloneNode(true)).parse();

  function getFileName(fileName) {
    var userAgent = window.navigator.userAgent,
        platform = window.navigator.platform,
        windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];

    if (windowsPlatforms.indexOf(platform) !== -1) {
      fileName = fileName.replace(':', '').replace(/[/\\?%*|"<>]/g, '-');
    } else {
      fileName = fileName.replace(':', '').replace(/\//g, '-').replace(/\\/g, '-');
    }
    return fileName;
  }
  const fileName = getFileName(title);

  if (selection) {
      var markdownify = selection;
  } else {
      var markdownify = content;
  }

  if (vault) {
      var vaultName = '&vault=' + encodeURIComponent(`${vault}`);
  } else {
      var vaultName = '';
  }

  const markdownBody = new Turndown({
      headingStyle: 'atx',
      hr: '~~~',
      bulletListMarker: '-',
      codeBlockStyle: 'fenced',
      emDelimiter: '*',
  }).turndown(markdownify);

  var date = new Date();

  function convertDate(date) {
    var yyyy = date.getFullYear().toString();
    var mm = (date.getMonth()+1).toString();
    var dd  = date.getDate().toString();
    var mmChars = mm.split('');
    var ddChars = dd.split('');
    return yyyy + '-' + (mmChars[1]?mm:"0"+mmChars[0]) + '-' + (ddChars[1]?dd:"0"+ddChars[0]);
  }

  const today = convertDate(date);

  /* YAML front matter as tags render cleaner with special chars  */
  const fileContent = 
      "---\n"
      + "author:    " + byline + "\n"
      + "title:     [" + title + "]\n"
      + "source:    " + document.URL + "\n"
      + "clipped:   " + today + "\n"
      + "published: \n\n" 
      + "tags:      [" + tags + "]\n"
      + "---\n\n"
      + markdownBody ;
  
  document.location.href = "obsidian://new?"
    + "file=" + encodeURIComponent(folder + fileName)
    + "&content=" + encodeURIComponent(fileContent)
    + vaultName ;
})

@skelly-larry
Copy link

Hey @jordanwiseman , unfortunately none of the variations work very well for me. In Chrome, I've regressed to all extensions disabled, security set to 'no protection', AV disabled, local Defender firewall disabled... still, a particular page will work once for me, then fail for awhile, then work again. For example:

https://xkcd.com/327/

I see the obsidian launch in the console, Sometimes with an error, but often with no error and still does not clip.

Sometimes see: see unknown origin/cancelled. However when I retry I do not see this, but see: Not allowed to launch 'obsidian://new?file=Exploits%20of%20a%20Mom&content=---...' because a user gesture is required.

There is no CORS error in the console. Of course not, the page origin is not making the request.

Some pages never work, always get the '... user gesture required'.
Like this page: https://sportsmedicine-open.springeropen.com/articles/10.1186/s40798-021-00390-y#author-information

Some pages work again, randomly. I could not find a pattern for awhile. Opening another page in the browser makes no difference. However, if I type into another page, like this post, or gmail... or even notepad, it almost always makes it work again. And it fails until I use the keyboard on a page again.

No joy in Edge either. Have only tried a personal machine so far.

I've wasted most of my morning on this, need to get some things done today!

@alessandro-fazzi
Copy link

@skelly-larry just FYI, with my variation the XKCD URL techincally works, but wrong portion of the page is extracted by Readability, thus resulting content is not meaningful (you can try it by https://downmark.herokuapp.com/obsidian?u=https://xkcd.com/327/). The second URL AFAIK doesn't work because the generated obsidian://... is too long to be handled by the browser; this could happen when you're clipping a URL with very long content. One example symptom I collected:

 curl -L "https://downmark.herokuapp.com/obsidian?u=https://sportsmedicine-open.springeropen.com/articles/10.1186/s40798-021-00390-y"
curl: (27) Out of memory

Since my variation has a server-side component, I can report - for curiosity sake - that the server is correctly producing the URL, but the client (curl or the browser) is not able to handle it.

Cheers

Disclaimer: I'm really not interested into bringing someone to use my personal experiment variation, just bringing some more data to the table to eventually contribute to the discussion. ;)

@jordanwiseman
Copy link

jordanwiseman commented Mar 5, 2022

Yeah, for the gesture error, this is a security feature, inconsistently applied, across browsers. Since the bookmarklet calls an external protocol handler ("obsidian://") , the usual policy is not to let this happen automatically, i.e., without the user's knowledge. If you interact with the page you want to clip after it's loaded, but before you click on the bookmarklet, it should work. Most tries, anyway. It worked for XKCD (although the actual comic image isn't grabbed, but that's an issue with Readability not understanding the regions on the xkcd pages).

Interaction can be as simple as scrolling the page, btw.

For the SpringerOpen link, it's as @pioneerskies said: the generated obsidian link is too large to be handled in this way (see https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers), although the entire constructed value does get logged to the console without error, ironically. There's no real way around this when using the obsidian protocol handler since you can't POST to a protocol handler (which is basically just a command line).

death_au's MarkDownload addon doesn't have these issues because addons don't have the same limitations as a bookmarklet running javascript "in" the page.

@skelly-larry
Copy link

Thanks @jordanwiseman . Yes the fact it logged the entire command properly threw me... but I was already late taking one of my kids to the airport so I had to abandon it. Never wrote javascript, might need to get into it, and look around for useful extensions. I and my kids have hundreds of recipes marooned in Evernote so I appreciate the local, replicable markdown. I'll mainly use this for annotated links, and I'm curious why the source tag is not stored as a navigable URL, but easy to enhance; maybe I'm misunderstanding. In a perfect world it would popup a dialog with meta tags I could prune, and the standard metadata, editable, before it commits. If the text selection is null, then grab the metadata and a URL link. Honestly, Obsidian is so flexible, I have to figure out how I plan to use it. The graph is cute, looks like a project I know Vasturiano's force graph that we have used before in the lab. Will have to play with it more to see if it is really useful.

@Write
Copy link

Write commented Mar 6, 2022

This script fail on almost every website for me due to CORS.
Only solution for me is to disable cors entierly in firefox
content.cors.disable to true

@skelly-larry
Copy link

After scanning some documentation, I think Obsidian has great potential but has ignored their most common use case. Maybe I am missing something? I hope I am.

The documentation describes a note-first approach, and a link-first approach. If you are taking notes independent of the rest of the world this is ok. But I read web pages that Google and new services push to me constantly, or search, and almost all of my notes are web page-first. I used to share these to Pocket (terrible organization of pages) or other things. I resort to sharing them to an email message to myself to read the next day. What I want to do is push them to Obsidian, refine the metadata, and use existing or new tags to fit them into the tagging scheme.

I rarely start with notes. I am absorbing info that I want to tag, develop insight from, and link into more complex structures later.

A lot of potential because it is so flexible, but it is so horribly awkward to use. I ctan be morphed into something highly usable, but I have a day job.

@skelly-larry
Copy link

skelly-larry commented Mar 7, 2022

@pioneerskies @jordanwiseman

Many things I want to clip are too long and it fails. I want to grab the leading part of a page and edit from there; or a selection.
I also want a navigable link to the page at the bottom of what I clip.

This refinement of Jordan's fits as much of the page as possible, assuming a safe URL length for most browsers is 2048; i trimmed it back to 2030 due to errors, which can URI encode 1200-1300 characters of source content. It also adds the link to source and a diagnostic of the number of characters of page content clipped.

I acknowledge that tags may be fractured and the last character of the page may be broken, if URI encoding it escapes it and I truncate in the middle. But it captures pages pretty well so far and there will always be some massaging on the Obsidian page.

Decrementing by 1 char each time seems a little wasteful, I used to use 100, but its just browser cycles and is instantaneous.

Still no CSP or CORS solution.

Code

javascript: Promise.all([import('https://unpkg.com/turndown@6.0.0?module'), import('https://unpkg.com/@tehshrike/readability@0.2.0'), ]).then(async ([{
    default: Turndown
}, {
    default: Readability
}]) => {

  /* Optional vault name */
  const vault = "";

  /* Optional folder name such as "Clippings/" */
  const folder = "";

  /* Optional tags  */
  let tags = "clippings";

  /* parse and lightly clean the site's meta keywords content into tags, if present */
  if (document.querySelector('meta[name="keywords" i]')) {
      var keywords = document.querySelector('meta[name="keywords" i]').getAttribute('content').split(',');

      keywords.forEach(function(keyword) {
          let tag = ' ' + keyword.split(' ').join('');
          tags += tag;
      });
  }

  function getSelectionHtml() {
    var html = "";
    if (typeof window.getSelection != "undefined") {
        var sel = window.getSelection();
        if (sel.rangeCount) {
            var container = document.createElement("div");
            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                container.appendChild(sel.getRangeAt(i).cloneContents());
            }
            html = container.innerHTML;
        }
    } else if (typeof document.selection != "undefined") {
        if (document.selection.type == "Text") {
            html = document.selection.createRange().htmlText;
        }
    }
    return html;
  }

  const selection = getSelectionHtml();

  const {
      title,
      byline,
      content
  } = new Readability(document.cloneNode(true)).parse();

  function getFileName(fileName) {
    var userAgent = window.navigator.userAgent,
        platform = window.navigator.platform,
        windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];

    if (windowsPlatforms.indexOf(platform) !== -1) {
      fileName = fileName.replace(':', '').replace(/[/\\?%*|"<>]/g, '-');
    } else {
      fileName = fileName.replace(':', '').replace(/\//g, '-').replace(/\\/g, '-');
    }
    return fileName;
  }
  const fileName = getFileName(title);

  if (selection) {
      var markdownify = selection;
  } else {
      var markdownify = content;
  }

  if (vault) {
      var vaultName = '&vault=' + encodeURIComponent(`${vault}`);
  } else {
      var vaultName = '';
  }

  const markdownBody = new Turndown({
      headingStyle: 'atx',
      hr: '~~~',
      bulletListMarker: '-',
      codeBlockStyle: 'fenced',
      emDelimiter: '*',
  }).turndown(markdownify);

  var date = new Date();

  function convertDate(date) {
    var yyyy = date.getFullYear().toString();
    var mm = (date.getMonth()+1).toString();
    var dd  = date.getDate().toString();
    var mmChars = mm.split('');
    var ddChars = dd.split('');
    return yyyy + '-' + (mmChars[1]?mm:"0"+mmChars[0]) + '-' + (ddChars[1]?dd:"0"+ddChars[0]);
  }

  const today = convertDate(date);

  /* YAML front matter as tags render cleaner with special chars  */
  const fileContent = 
      "---\n"
      + "author:    " + byline + "\n"
      + "title:     [" + title + "]\n"
      + "source:    " + document.URL + "\n"
      + "clipped:   " + today + "\n"
      + "published: \n\n" 
      + "tags:      [" + tags + "]\n"
      + "---\n\n"
      + markdownBody ;
  
  
  /* add a link to the source at the end */
  const linkref = 
      "\n\n---\n\source: [" + title + "](" + document.URL + ")\n";

  /* assemble URL, decrementing each time, until it does not exceed the max URL length for this browser, supposedly 2048 but allow for a little error with 2030 */
  contentLength = 2048;
  maxURLLength = 2030;
  decrement = 1;

  do {
	hrefString = "obsidian://new?"
      + "file=" + encodeURIComponent(folder + fileName)
      + "&content=" + encodeURIComponent(fileContent.substr(0,contentLength-1)) + encodeURIComponent(linkref) + "\n\nclipped " + String(contentLength) + " chars " 
      + vaultName ;
	contentLength = contentLength - decrement;
  } while (hrefString.length > maxURLLength);

  document.location.href = hrefString;

})

@alessandro-fazzi
Copy link

@skelly-larry nice one. I'll think about implementing a simple "selection" function on my script too. With something like https://jsfiddle.net/Y4BBq/

Still no CSP or CORS solution.

That's not a script's problem, but a security enforcement on some websites. I wrote a webservice for me only to be able to overcome that thing (I often do webclips from GitHub for example).

@wealthychef1
Copy link

Another workaround I've found for certain sites that block web clipping: use "Reader View" on Safari and copy and paste from there... defeats a lot of things

@vii33
Copy link

vii33 commented Mar 12, 2022

@jordanwiseman: Works like a charm, thank you. 👍

Small improvement idea: Add a level one headline (# The Headline) at the beginning of the document.
E.g.:

var headline = title;
const fileContent = ....... +
'# ' + headline + 
markdownBody;

@Write
Copy link

Write commented Mar 12, 2022

If this is turned into an extension, couldn't it bypass CORS restriction ?

@ScootRay
Copy link

@kepano, I have been using this and I have couple of bookmarklets to clip the contents into different folders in my vault. Since I created bookmarklets for various folders, I created a simple UI to fill in the vault name, folder name, tags and get the bookmarklet.

http://obsidian-clipper-maker.ganesshkumar.com

Let me know if this is useful for you guys!

Thanks for setting that up, makes it so much easier : )

Ray

@mathisgauthey
Copy link

mathisgauthey commented Jul 22, 2022

Is it supposed to work on Android? I'm using Brave, and saved the bookmark, but it doesn't do anything. Thanks in advance for your work!
I get the following errors :

VM184:1 Refused to load the script 'https://unpkg.com/turndown@6.0.0?module' because it violates the following Content Security Policy directive: "script-src github.githubassets.com". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

(anonymous) @ VM184:1
VM184:1 Refused to load the script 'https://unpkg.com/@tehshrike/readability@0.2.0' because it violates the following Content Security Policy directive: "script-src github.githubassets.com". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

(anonymous) @ VM184:1
90c05f1…:1 Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: https://unpkg.com/turndown@6.0.0?module

@aancw
Copy link

aancw commented Aug 20, 2022

I'm experiencing the issue that selection mode is not working on Safari. But work very well on chrome and firefox.

Ref: mdn/browser-compat-data#14270 (comment)

Edit: I think the problem is when clicking bookmarklet the selection is cleared because safari popup notification for obsidian

Screen Shot 2022-08-20 at 16 57 01

@leotulipan
Copy link

leotulipan commented Oct 17, 2022

On my Windows 10 with Google Chrome I had to add this to the registry and then it works.

Got the tip via this thread and used the Obsidian Clipper Maker

obsidan-allow.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome]

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\AutoOpenAllowedForURLs]
"1"="obsidian://"

@stormer
Copy link

stormer commented Oct 24, 2022

This is working great in Chrome but I'm wondering whether it's possible to get it working in Safari - I've tried the same procedure in Safari but to no avail.

@wealthychef1
Copy link

This is working great in Chrome but I'm wondering whether it's possible to get it working in Safari - I've tried the same procedure in Safari but to no avail.

I use it in Safari every day...

@stormer
Copy link

stormer commented Oct 24, 2022

@wealthychef1, maybe it's a Mac thing (I'm on a Mac). Still working great in Chrome but in Safari the load bar starts but then gets stuck.

@robertkstarr
Copy link

I'm getting an error on some websites because the titles can't contain certain characters. Is there a regex expression you can insert somewhere to prevent that error?

@wealthychef1
Copy link

@wealthychef1, maybe it's a Mac thing (I'm on a Mac). Still working great in Chrome but in Safari the load bar starts but then gets stuck.

I'm using Safari Version 16.0 (17614.1.25.9.10, 17614) on MacOS Monterey 12.6

@sfield541
Copy link

How can I fix the errors found in my developers console? The web clippings do not save. Thanks

@Write
Copy link

Write commented Oct 30, 2022

How can I fix the errors found in my developers console? The web clippings do not save. Thanks
Can you post your console logs ?

I'm afraid it's a CSP issue, then it's not fixable.

@vitr
Copy link

vitr commented Nov 6, 2022

Sorry for the irrelevant issue, but why does the bookmarklet not work with this page or any other gist or GitHub page?

@alessandro-fazzi
Copy link

Sorry for the irrelevant issue, but why does the bookmarklet not work with this page or any other gist or GitHub page?

@vitr That's the same answer as in https://gist.github.com/kepano/90c05f162c37cf730abb8ff027987ca3?permalink_comment_id=4352531#gistcomment-4352531

GitHub's CSPs cannot be "bypassed" w/o using a server side. I had to wrote a personal webservice in order to make this work w/ GitHub and some other sites (https://github.com/pioneerskies/downmark#backstory-and-credits JFYI)

P.S.: Heroku is going to shut down free tier hosting, so don't think to use my experimental publix webservice: it will be shut down and maybe moved away sooner than later

@vitr
Copy link

vitr commented Nov 8, 2022

thanks, I guess, browser extensions (e.g. https://github.com/deathau/markdownload) work with any page, as they have full access to the content

@alessandro-fazzi
Copy link

thanks, I guess, browser extensions (e.g. https://github.com/deathau/markdownload) work with any page, as they have full access to the content

You're right @vitr (AFAIK)! If that fits your flow, than it seems a sturdy option to me

@camayl
Copy link

camayl commented Dec 21, 2022

Hi, thanks for this great plugin! I have a small issue. Somehow images are not exported to Obsidian from voxeu (an economics website). I tried different articles from there, but they always export without pics. Here is an example site: https://cepr.org/voxeu/columns/addressing-uks-public-finances-after-mini-budget-crisis

Any way to remedy this?

@frenksisco
Copy link

I just can't get this bookmarklet to work with any of the sites I read.
When I click it absolutely nothing happens. No notes appear in obsidian. I use Brave browser.
"MarkDownload" and "Obsidian Web" extensions for chrome work.

@shutterkurt
Copy link

thanks for creating this - one FYI for chrome on Android. I was running into a max length issue for the generated bookmarklet URI. Seems like URI couldn't be > 5000 characters (when pasting in the javascript code, the text box (?) was limiting the # of characters allowed, truncating to about 5000 chars). So nothing worked - no clipping nor errors.

I didn't spend too much time but my workaround was to delete/modify a couple things from javascript (content template, comments) and re generate with the bookmarklet maker linked above. Now generated is ~4800 chars and whole pages seem to work but not selected subset. Good enough for me at the moment. (Android 12, samsung tablet)

@kvdogan
Copy link

kvdogan commented Mar 22, 2023

Thanks for sharing this, i had faced one problems with the script by @kepano for generating markdown for long pages, it was silently failing and the way few characters clipped with the implementation of @skelly-larry . I used implemetation from @kepano, @jordanwiseman and @skelly-larry as input (thanks to all of them) and improved the script by removing character limitation for full page clipping with the aid of advanced uri plugin, meaning Obsidian Advanced URI plugin must be installed and enabled. Also selected range of text and tagging work as good as other implementations. Tested on Chrome and Firefox with no issue. Hope that helps to those who have not made the previous implementations work for them and those who have been looking for a full scale implementation.

javascript: Promise.all([import('https://unpkg.com/turndown@6.0.0?module'), import('https://unpkg.com/@tehshrike/readability@0.2.0'),]).then(async ([{
    default: Turndown
}, {
    default: Readability
}]) => {

    /* Optional vault name */
    const vault = "";

    /* Optional folder name such as "Clippings/" */
    const folder = "";

    /* Optional tags  */
    let tags = "clippings";

    /* parse and lightly clean the site's meta keywords content into tags, if present */
    if (document.querySelector('meta[name="keywords" i]')) {
        var keywords = document.querySelector('meta[name="keywords" i]').getAttribute('content').split(',');

        keywords.forEach(function (keyword) {
            let tag = ' ' + keyword.split(' ').join('');
            tags += tag;
        });
    }

    function getSelectionHtml() {
        var html = "";
        if (typeof window.getSelection != "undefined") {
            var sel = window.getSelection();
            if (sel.rangeCount) {
                var container = document.createElement("div");
                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                    container.appendChild(sel.getRangeAt(i).cloneContents());
                }
                html = container.innerHTML;
            }
        } else if (typeof document.selection != "undefined") {
            if (document.selection.type == "Text") {
                html = document.selection.createRange().htmlText;
            }
        }
        return html;
    }

    const selection = getSelectionHtml();

    const {
        title,
        byline,
        content
    } = new Readability(document.cloneNode(true)).parse();

    function getFileName(fileName) {
        var userAgent = window.navigator.userAgent,
            platform = window.navigator.platform,
            windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];

        if (windowsPlatforms.indexOf(platform) !== -1) {
            fileName = fileName.replace(':', '').replace(/[/\\?%*|"<>]/g, '-');
        } else {
            fileName = fileName.replace(':', '').replace(/\//g, '-').replace(/\\/g, '-');
        }
        return fileName;
    }
    const fileName = getFileName(title);

    if (selection) {
        var markdownify = selection;
    } else {
        var markdownify = content;
    }

    if (vault) {
        var vaultName = '&vault=' + encodeURIComponent(`${vault}`);
    } else {
        var vaultName = '';
    }

    const markdownBody = new Turndown({
        headingStyle: 'atx',
        hr: '~~~',
        bulletListMarker: '-',
        codeBlockStyle: 'fenced',
        emDelimiter: '*',
    }).turndown(markdownify);

    var date = new Date();

    function convertDate(date) {
        var yyyy = date.getFullYear().toString();
        var mm = (date.getMonth() + 1).toString();
        var dd = date.getDate().toString();
        var mmChars = mm.split('');
        var ddChars = dd.split('');
        return yyyy + '-' + (mmChars[1] ? mm : "0" + mmChars[0]) + '-' + (ddChars[1] ? dd : "0" + ddChars[0]);
    }

    const today = convertDate(date);

    /* YAML front matter as tags render cleaner with special chars  */
    const fileContent =
        "---\n"
        + "author:    " + byline + "\n"
        + "title:     [" + title + "]\n"
        + "source:    " + document.URL + "\n"
        + "clip date: " + today + "\n"
        + "published: \n\n"
        + "tags:      [" + tags + "]\n"
        + "---\n\n"
        + markdownBody
        + "\n\n---\n" + String(markdownBody.length) + " chars clipped from "
        + "[" + title + "](" + document.URL + ")\n";


    // This function must be called in a visible page, such as a browserAction popup
    // or a content script. Calling it in a background page has no effect!
    async function copyContent() {
        try {
            await navigator.clipboard.writeText(fileContent);
        } catch (err) {
            console.error('Failed to copy: ', err);
        }
    }

    copyContent();

    document.location.href = "obsidian://advanced-uri?"
        + "vault=" + vaultName
        + "&clipboard=true"
        + "&mode=new"
        + "&filepath=" + encodeURIComponent(folder + fileName);

})

@fabioscarsi
Copy link

But could I launch any of your scripts through Keyboard Maestro? I'm not a coder. Sorry if the question was silly.

@yagmurx
Copy link

yagmurx commented Nov 16, 2023

Hi, thanks for awesome codes! I am an Arc Browser user and bookmarklet doesn't work for me. I turn it an extension (dev-mode) by using this page, want to share everyone.
Bookmarklet2Extension:https://sandbox.self.li/bookmarklet-to-extension/

@pascalandy
Copy link

Awesome, thank you very much !

@philips
Copy link

philips commented Nov 28, 2023

With content security policy enforcement in Safari this needs to embed the packages instead of importing them. My gist over here fixes it:

https://gist.github.com/philips/8f4e93cb2be2428344ece63aecca7941

https://github.blog/2013-04-19-content-security-policy/
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

[Error] Refused to load https://unpkg.com/turndown@6.0.0?module because it does not appear in the script-src directive of the Content Security Policy.
[Error] Refused to load https://unpkg.com/@tehshrike/readability@0.2.0 because it does not appear in the script-src directive of the Content Security Policy.

@src4026
Copy link

src4026 commented Dec 3, 2023

Hi, I wanted to post an "Issue" regarding the plugin.
I've been using it for a while and it's amazing! However, I have found that sometimes when it clips webpages that have a ? in their title (this could extend to other reserved characters as well), it grabs the same and sets it as the title. This brought up some issues where Obsidian and other programs behaved weirdly. Removing the question mark solved the problem. It would be nice to have to not clip these characters.

@Maahantuoja
Copy link

Hi, I'm using the opera browser, but I can't get this to work properly. If I capture only some part of the page content then everything goes ok, but if I try to capture the whole page then it just closes the tab, but doesn't save anything.

I greatly appreciate it if someone could help me with this problem.

@bramhooimeijer
Copy link

Hi!

A small bug in the gist:

      fileName = fileName.replace(':', '').replace(/\//g, '-').replace(/\\/g, '-');

Here you only replace the first occurence of :, leaving the rest as a possible issue.

      fileName = fileName.replace(/:/g, '').replace(/\//g, '-').replace(/\\/g, '-');

Should fix that.

@Jerwins
Copy link

Jerwins commented Jan 30, 2024

On my Windows 10 with Google Chrome I had to add this to the registry and then it works.

Got the tip via this thread and used the Obsidian Clipper Maker

obsidan-allow.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome]

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\AutoOpenAllowedForURLs]
"1"="obsidian://"

Thanks, this worked.

@KernelBypass
Copy link

KernelBypass commented Jan 30, 2024

With content security policy enforcement in Safari this needs to embed the packages instead of importing them. My gist over here fixes it:

https://gist.github.com/philips/8f4e93cb2be2428344ece63aecca7941

https://github.blog/2013-04-19-content-security-policy/ https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

[Error] Refused to load https://unpkg.com/turndown@6.0.0?module because it does not appear in the script-src directive of the Content Security Policy. [Error] Refused to load https://unpkg.com/@tehshrike/readability@0.2.0 because it does not appear in the script-src directive of the Content Security Policy.

Kepano's OG script works fine for me in MacOS Safari, but yours doesn't seem to fit into the bookmark's address field even when minified.

  1. Am I doing something incorrectly? I copy your gist -> https://caiorss.github.io/bookmarklet-maker/ -> insert into a bookmark. Pretty straightforward, but yours won't paste fully.
  2. What is the Safari content security policy enforcement issue contingent upon? Because I do not seem to have that issue.

Both work great in Firefox though.

@Kmfernan5
Copy link

@dbdennis
Copy link

dbdennis commented Feb 9, 2024

On my Windows 10 with Google Chrome I had to add this to the registry and then it works.
Got the tip via this thread and used the Obsidian Clipper Maker
obsidan-allow.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome]

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\AutoOpenAllowedForURLs]
"1"="obsidian://"

Thanks, this worked.

Thank you so much for this excellent clipper!! It is working wonderfully with chrome and two macbooks. It isn't working on chrome and a Windows 10 setup. I did these steps and there are no errors in the Developers Console when I run the original "Clip in Obsidian" or a modified one from the Maker (specifying a Vault and Clippings folder). Again, both work perfectly on Macs. But even though it is clean on the chrome console on the Windows machine (helping a friend), Obsidian doesn't open and the page isn't copied. (If I just do `obsidian://' in the chrome url bar, it does open, so I don't think it is a security setting.)

Any suggestions highly appreciated!

@jmiralva
Copy link

Hi, thanks for awesome codes! I am an Arc Browser user and bookmarklet doesn't work for me. I turn it an extension (dev-mode) by using this page, want to share everyone. Bookmarklet2Extension:https://sandbox.self.li/bookmarklet-to-extension/

@yagmurx does this still work for you? i'm trying to use it now but can't get it to work. any tips? thanks!

@aREversez
Copy link

It works well with FireFox, but doesn't work with my Chrome. I don't know why..

@aREversez
Copy link

On my Windows 10 with Google Chrome I had to add this to the registry and then it works.

Got the tip via this thread and used the Obsidian Clipper Maker

obsidan-allow.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome]

[HKEY_CURRENT_USER\SOFTWARE\Policies\Google\Chrome\AutoOpenAllowedForURLs]
"1"="obsidian://"

I tried but it still didn't work. Do you know why?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment