Skip to content

Instantly share code, notes, and snippets.

@kepano
Last active July 16, 2024 14:14
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
@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?

@saifsmailbox98
Copy link

saifsmailbox98 commented Jun 21, 2024

Simplified version that only saves the following: title, url, local timestamp (e.g., 2024-06-21T06:12:25)

javascript:(function(){
    const now = new Date();
    const pad = (num) => String(num).padStart(2, '0');
    const formattedDate = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;

    const title = document.title;
    const url = document.URL;

    const folder = "00 - 09 System/04 Bookmarks/04.01 Obsidian Web Clipper/Raw Notes/";
    const fileName = `${title}`;

    const fileContent = 
        '---\n'
        + `url: ${url}\n`
        + `date: ${formattedDate}\n`
        + '---\n\n';

    document.location.href = "obsidian://new?"
        + "file=" + encodeURIComponent(folder + fileName)
        + "&content=" + encodeURIComponent(fileContent);
})();

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