#productivity #note-taking #visualisation
Github code is here.
The goal of this project is to create an Obsidian plugin that allows users to embed local HTML files into their Markdown notes using iframes. This is useful for displaying interactive content like maps, charts, or custom HTML pages directly within your Obsidian notes.
Important: The iframe embed is visible in “reading mode” and not in “editing mode”.
This guide is written for complete beginners, including people who have never used npm (like me), written TypeScript (like me), or built an Obsidian plugin (like me) before. Each step is broken down clearly and includes detailed explanations, comments, and online resources. I heavily relied on co-pilot to write this plugin.
Languages:
TypeScript: A superset of JavaScript that adds static types. It helps catch errors early and improves code clarity. Learn more
CSS: Used to style iframe elements.
JSON: A data format used for configuration files (e.g., manifest.json).
Tools & Platforms:
Obsidian API: Framework provided by Obsidian for writing plugins. Obsidian API Docs
npm: Node Package Manager, used to install and manage JavaScript packages. Comes with Node.js.
esbuild: A super-fast JavaScript bundler and compiler. esbuild Docs
Node.js: A JavaScript runtime environment. Required for running npm. Node.js website
The DOM (Document Object Model) is a representation of your web page structured like a tree. Each HTML tag becomes a node in this tree. Browsers build this model from the HTML code.
This model lets you use JavaScript (or TypeScript) to read or change parts of the web page.
💡 Think of HTML as the blueprint of a house, and the DOM as the actual house that’s built from that blueprint. Once the house is built, you can move walls, add furniture, or even change the layout — just like how the DOM allows you to interact with and modify the web page after it’s loaded.
DOM methods are JavaScript functions used to create, find, or manipulate elements in the DOM.
document.createElement('iframe')
→ Creates a new iframe element
element.querySelectorAll('p')
→ Finds all <p>
elements in the rendered Markdown
p.replaceWith(iframe)
→ Replaces a paragraph node with the iframe
Here’s the folder structure for your plugin:
nat-iframe-renderer/
├── main.js # Compiled JavaScript file (auto-generated)
├── main.js.map # Source map (helps with debugging)
├── manifest.json # Metadata that Obsidian reads
├── package.json # Defines your project and dependencies
├── README.md # Optional: plugin documentation
├── rollup.config.js # Optional: config for Rollup bundler (if used)
├── styles.css # CSS styles for the iframe
├── tsconfig.json # TypeScript configuration
└── src/
└── main.ts # Main plugin logic (written in TypeScript)
First, you need to create a folder inside your Obsidian vault where plugins go.
cd /Users/nat/Dropbox/Nat_Arslan_Blog/.obsidian/plugins # Navigate to your plugins folder
mkdir nat-iframe-renderer # Make a folder for your plugin
cd nat-iframe-renderer # Go into that folder
🔍 Tip: Replace
/Users/nat/...
with your actual vault path.
This will create a package.json
file, which defines your project.
npm init -y
📦
npm
stands for Node Package Manager. It helps install and manage packages (tools/code) from the internet.✅
-y
means “yes to all questions” — it skips the setup prompts and creates a basicpackage.json
instantly.
These tools are required to build and run your plugin.
npm install obsidian # Gives access to Obsidian's API
npm install --save-dev typescript esbuild # Installs TypeScript + esbuild as development tools
obsidian
: Gives access to Obsidian’s API.
typescript
: Allows us to write TypeScript code.
esbuild
: Compiles TypeScript into JavaScript very quickly.
manifest.json
This file tells Obsidian how to use your plugin.
It is essential for Obsidian to recognize the plugin and load its metadata correctly. Without this file, the plugin won’t appear in the Community Plugins section, even if the code is present.
Create a new file manifest.json
and paste this:
{
"id": "nat-iframe-renderer",
"name": "Nat Iframe Renderer",
"version": "1.0.0",
"minAppVersion": "0.12.0",
"description": "An Obsidian plugin to render local HTML files in markdown files using iframes.",
"author": "Nat",
"authorUrl": "https://yourwebsite.com",
"isDesktopOnly": false,
"main": "main.js",
"css": "styles.css"
}
main.ts
)If you haven’t already, create a folder named src
inside your nat-iframe-renderer
project directory:
mkdir src
Then, create a new file src/main.ts
and paste the following code.
const { Plugin } = require('obsidian');
— Imports the Plugin
class from the Obsidian API so we can create our own plugin by extending it. Learn more
module.exports = class IframeRenderer extends Plugin { ... }
— This exports your custom plugin so Obsidian can recognize and use it. You’re creating a class that extends Obsidian’s built-in Plugin class (i.e., it inherits its functionality).
async onload()
— This function is automatically called by Obsidian when your plugin starts. It’s where you put all your plugin initialization logic. Async functions let you use await
inside them.
this.registerMarkdownPostProcessor(...)
— This method lets you add custom behavior after Obsidian renders a note. We use it to detect and transform custom syntax like !iframe[...]
. Docs on post processors
element.querySelectorAll('p')
— This finds all <p>
tags (paragraphs) in the rendered note. MDN reference
iframe.src = this.app.vault.adapter.getResourcePath(fileName);
— This constructs the correct local file path for the iframe src
using Obsidian’s API. It’s how you safely link to a file inside the vault.
p.replaceWith(iframe);
— This replaces the original paragraph (e.g., !iframe[mychart.html]
) with the new iframe element.
🧠 These are all standard JavaScript/TypeScript and DOM methods. Once you’re comfortable reading this code, you can do much more with plugins!
const { Plugin } = require('obsidian'); // Import Plugin class from Obsidian
// Export our custom plugin class
module.exports = class IframeRenderer extends Plugin {
// This function is called when the plugin loads in Obsidian
async onload() {
// Register a Markdown post-processor (runs after rendering a note)
this.registerMarkdownPostProcessor((element) => {
// Look through all paragraph tags (<p>)
const matches = element.querySelectorAll('p');
// Check each paragraph for the custom iframe syntax
matches.forEach((p) => {
// Match lines like: !iframe[my-file.html]
const match = p.textContent?.match(/^!iframe\[(.+)\]$/);
if (match) {
const fileName = match[1].trim(); // Extract the filename
// Create a new <iframe> HTML element
const iframe = document.createElement('iframe');
// Set the iframe's source to the file path in the vault
iframe.src = this.app.vault.adapter.getResourcePath(fileName);
// Apply some default styles
iframe.style.border = '2px solid red'; // Adds a red border around the iframe for debugging; you can remove or change this style later for production use
iframe.style.borderRadius = '4px';
iframe.style.minHeight = '300px';
iframe.style.width = '100%';
iframe.style.height = '500px';
// Replace the paragraph with the iframe
p.replaceWith(iframe);
}
});
});
}
};
🧠 What’s Happening Here?
The plugin looks for lines like
!iframe[myfile.html]
in your notes.It replaces that line with an iframe showing the specified HTML file.
tsconfig.json
Create a tsconfig.json
file to tell TypeScript how to compile your code:
{
"compilerOptions": {
"target": "ES2020", // Which JavaScript version to compile to
"module": "ESNext", // Module system to use
"moduleResolution": "node", // Use Node.js style module resolution
"strict": true, // Enable all strict type checks
"esModuleInterop": true, // Allow mixing CommonJS and ES modules
"skipLibCheck": true, // Skip checking library types
"forceConsistentCasingInFileNames": true,
"outDir": "." // Output files in root
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
package.json
Build ScriptAdd a build script to compile the plugin (inside the scripts
section of your package.json
file):
"scripts": {
"build": "esbuild src/main.ts --bundle --format=cjs --platform=node --external:obsidian --target=es2020 --outfile=main.js"
}
🛠 This command compiles
src/main.ts
intomain.js
using esbuild.
npm run build # This will compile your TypeScript into JavaScript
If everything works, you’ll see a new main.js
file.
Open Obsidian.
Go to Settings > Community Plugins.
Disable Safe Mode if it’s on.
Click “Load Unpacked Plugin”.
Select your nat-iframe-renderer
folder.
Toggle the plugin ON.
Create a new Markdown note.
Type the following:
!iframe[my-chart.html]
Make sure my-chart.html
exists in your vault.
When you preview the note (reading mode), it should show the HTML inside an iframe.
🌐 Further Reading & Resources