SEO blogpost
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
---
|
||||
title: "How to Build a Lightweight Chrome Extension (Manifest V3) for Real-Time Image & Canvas Inspection"
|
||||
description: "A step-by-step developer guide to building a Manifest V3 browser extension with image scanning, Canvas extraction, context menu triggers, and shadow DOM overlays."
|
||||
tags: ["chromeextension", "javascript", "webdev", "browser"]
|
||||
canonical_url: "https://greenlenspro.com/"
|
||||
cover_image: "https://greenlenspro.com/images/blog/chrome-extension-manifest-v3.jpg"
|
||||
---
|
||||
|
||||
# How to Build a Lightweight Chrome Extension (Manifest V3) for Real-Time Image & Canvas Inspection
|
||||
|
||||
Browser extensions are one of the most effective ways to make AI models immediately accessible to users across the web. Instead of navigating to a dedicated web app, users can inspect images, scan elements (`pflanzen scanner`), or perform visual AI queries on any web page with a single right-click or hover action.
|
||||
|
||||
However, migrating to or building on **Chrome Extension Manifest V3 (MV3)** introduces strict security policies, background service worker lifecycles, and CORS restrictions that break traditional DOM scraping techniques.
|
||||
|
||||
In this deep-dive tutorial, we'll walk through the implementation of a zero-dependency Chrome extension inspired by the [GreenLens Chrome Extension](https://greenlenspro.com/). You will learn how to set up Manifest V3 service workers, extract cross-origin images or Canvas elements without triggering CORS errors, and render isolated hover UI overlays using the Shadow DOM.
|
||||
|
||||
---
|
||||
|
||||
## 1. Extension Architecture under Manifest V3
|
||||
|
||||
Under Manifest V3, background scripts no longer run persistently in a DOM-enabled background page. Instead, they operate as **ephemeral Service Workers** that spin down after periods of inactivity.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[DOM / Webpage Image] -->|Hover / Context Menu| B[Content Script `scan.js`]
|
||||
B -->|Chrome Message Passing| C[MV3 Service Worker `background.js`]
|
||||
C -->|API Fetch / Model Inference| D[Remote AI API / GreenLens Backend]
|
||||
D -->|Diagnostic Payload| C
|
||||
C -->|Message Response| E[Shadow DOM Overlay in `scan.js`]
|
||||
```
|
||||
|
||||
### Key Components:
|
||||
- **`manifest.json`**: Declares permissions (`contextMenus`, `activeTab`, `scripting`, `storage`).
|
||||
- **`background.js` (Service Worker)**: Registers context menus, handles message queues, and dispatches external API requests.
|
||||
- **`scan.js` (Content Script)**: Injected into host pages, inspects hovered elements, captures Canvas data, and renders Shadow DOM overlays.
|
||||
- **`popup.js` / `popup.html`**: Extension popup UI for quick status checks and manual URL uploads.
|
||||
|
||||
---
|
||||
|
||||
## 2. Defining Manifest V3 (`manifest.json`)
|
||||
|
||||
To inspect image elements on web pages (`pflanze erkennen`), your manifest must declare explicit permissions for host access while maintaining minimal scope for Chrome Web Store approval.
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "GreenLens - Instant Plant Scanner & Disease Identifier",
|
||||
"version": "1.0.1",
|
||||
"description": "Scan any plant image or flower photo across the web to identify species and diagnose health symptoms.",
|
||||
"permissions": [
|
||||
"contextMenus",
|
||||
"activeTab",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://*/*",
|
||||
"http://*/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["shared.js", "scan.js"],
|
||||
"css": ["scan.css"]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": "icons/icon-48.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Background Service Worker & Context Menu Setup (`background.js`)
|
||||
|
||||
The service worker creates a custom context menu item when the user right-clicks any image on a website. When clicked, it passes the target image URL or data URI to your vision recognition backend.
|
||||
|
||||
```javascript
|
||||
// background.js - MV3 Service Worker
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: "greenlens-scan-image",
|
||||
title: "🌱 Scan with GreenLens (Identify & Diagnose)",
|
||||
contexts: ["image"]
|
||||
});
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.menuItemId === "greenlens-scan-image" && tab?.id) {
|
||||
const imageUrl = info.srcUrl;
|
||||
|
||||
// Send message to content script to display loading state
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
action: "INITIATE_SCAN",
|
||||
imageUrl: imageUrl
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch("https://greenlenspro.com/v1/scan", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ imageUrl: imageUrl })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Dispatch results back to content script
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
action: "RENDER_RESULT",
|
||||
result: data
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Scan API Error:", error);
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
action: "SCAN_ERROR",
|
||||
error: "Failed to scan target image."
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Extracting Canvas & CORS Images in Content Script (`scan.js`)
|
||||
|
||||
Websites often render images inside `<canvas>` tags or block direct CORS fetching via `crossorigin="anonymous"`. To bypass CORS hurdles safely without proxying, content scripts can convert image elements into base64 Data URIs directly inside the client browser.
|
||||
|
||||
```javascript
|
||||
// scan.js - Content Script Element Extraction
|
||||
|
||||
function convertElementToDataUri(imgElement) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Handling standard <img> tags
|
||||
if (imgElement.tagName.toLowerCase() === 'img') {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.width = imgElement.naturalWidth || imgElement.width;
|
||||
canvas.height = imgElement.naturalHeight || imgElement.height;
|
||||
|
||||
try {
|
||||
ctx.drawImage(imgElement, 0, 0);
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.85));
|
||||
} catch (err) {
|
||||
// Tainted canvas fallback: return src URL directly
|
||||
resolve(imgElement.src);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handling HTML5 Canvas elements
|
||||
if (imgElement.tagName.toLowerCase() === 'canvas') {
|
||||
try {
|
||||
resolve(imgElement.toDataURL('image/jpeg', 0.85));
|
||||
} catch (err) {
|
||||
reject(new Error("Canvas tainted by cross-origin data."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error("Unsupported element type for scanning."));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Isolated UI Overlays using Shadow DOM
|
||||
|
||||
Injecting popup UI overlays directly into arbitrary third-party web pages usually results in CSS style leaks. Global stylesheets from the host site can ruin your extension's typography, buttons, and layout.
|
||||
|
||||
The solution is wrapping your UI in an isolated **Shadow DOM root**.
|
||||
|
||||
```javascript
|
||||
// scan.js - Isolated Shadow DOM Popup Injector
|
||||
|
||||
function injectResultModal(scanData) {
|
||||
let hostElement = document.getElementById('greenlens-shadow-host');
|
||||
|
||||
if (!hostElement) {
|
||||
hostElement = document.createElement('div');
|
||||
hostElement.id = 'greenlens-shadow-host';
|
||||
document.body.appendChild(hostElement);
|
||||
}
|
||||
|
||||
// Create shadow root if it doesn't already exist
|
||||
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' });
|
||||
|
||||
shadowRoot.innerHTML = `
|
||||
<style>
|
||||
.gl-card {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 320px;
|
||||
background: #ffffff;
|
||||
color: #16181d;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
z-index: 999999;
|
||||
}
|
||||
.gl-title { font-size: 16px; font-weight: 700; color: #16794a; margin: 0 0 6px; }
|
||||
.gl-sub { font-size: 13px; color: #4a4f5a; margin-bottom: 12px; }
|
||||
.gl-badge { display: inline-block; background: #e8f4ed; color: #16794a; padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: 600; }
|
||||
.gl-close { float: right; cursor: pointer; border: 0; background: none; font-size: 16px; color: #888; }
|
||||
</style>
|
||||
<div class="gl-card">
|
||||
<button class="gl-close" onclick="this.getRootNode().host.remove()">×</button>
|
||||
<div class="gl-badge">${scanData.confidence || '98% Match'}</div>
|
||||
<h3 class="gl-title">${scanData.species || 'Pflanze erkannt'}</h3>
|
||||
<p class="gl-sub">${scanData.diagnosis || 'Healthy foliage detected.'}</p>
|
||||
<a href="https://greenlenspro.com" target="_blank" style="color:#16794a; font-weight:600; font-size:12px; text-decoration:none;">View complete care guide →</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Comparing Chrome Extension Scanners vs. Google Lens
|
||||
|
||||
While generic visual search engines like `google lens pflanzen erkennen` return broad web image matches, a domain-tailored Chrome extension offers specialized capabilities:
|
||||
|
||||
| Feature | Generic Visual Search (`google lens`) | Specialized MV3 Extension (GreenLens) |
|
||||
|---|---|---|
|
||||
| **Species Identification (`pflanzen bestimmen`)** | Broad web search matches | Specialized Botanical Taxonomy Models |
|
||||
| **Health Diagnosis (`pflanzenkrankheiten erkennen`)** | Limited to visual similarity | Multi-symptom chlorosis & pest detection |
|
||||
| **Context Integration** | Opens external tab | In-page Shadow DOM Overlay |
|
||||
| **Canvas & WebGL Support** | No direct element inspection | Client-side Data URI canvas extraction |
|
||||
|
||||
---
|
||||
|
||||
## Summary & Developer Checklist
|
||||
|
||||
1. **Adopt MV3 Service Workers:** Treat background tasks as stateless, event-driven functions.
|
||||
2. **Prevent CSS Contamination:** Always use `attachShadow({ mode: 'open' })` for injected content script UI elements.
|
||||
3. **Handle Canvas Fallbacks:** Gracefully degrade between DOM src attributes, canvas data URIs, and remote URLs.
|
||||
4. **Minimal Permissions Strategy:** Request only `contextMenus` and `activeTab` to ensure rapid Chrome Web Store review.
|
||||
|
||||
To see a live implementation of an in-browser plant scanner, test out the [GreenLens Browser Extension Engine](https://greenlenspro.com/).
|
||||
Reference in New Issue
Block a user