Architecture Documentation: - System component overview (logging, theme, build) - Content processing pipeline details - File structure and organization - Message flow diagrams - Storage strategy (local vs sync) - MV3 constraints and solutions Migration Patterns: - 8 common MV2 → MV3 migration patterns - TypeScript examples with proper error handling - Chrome API usage examples - Best practices for each scenario Serves as reference to avoid re-explaining systems repeatedly.
23 KiB
Development Guide - Trilium Web Clipper MV3
Practical guide for common development tasks and workflows.
Daily Development Workflow
Starting Your Session
# Navigate to project
cd apps/web-clipper-manifestv3
# Start development build (keep this running)
npm run dev
# In another terminal (optional)
npm run type-check --watch
Loading Extension in Chrome
- Open
chrome://extensions/ - Enable "Developer mode" (top right)
- Click "Load unpacked"
- Select the
dist/folder - Note the extension ID for debugging
Development Loop
1. Make code changes in src/
↓
2. Build auto-rebuilds (watch mode)
↓
3. Reload extension in Chrome
- Click reload icon on extension card
- Or Ctrl+R on chrome://extensions/
↓
4. Test functionality
↓
5. Check for errors
- Popup: Right-click → Inspect
- Background: Extensions page → Service Worker → Inspect
- Content: Page F12 → Console
↓
6. Check logs via extension Logs page
↓
7. Repeat
Common Development Tasks
Task 1: Add a New Capture Feature
Example: Implementing "Save Tabs" (bulk save all open tabs)
Steps:
-
Reference the MV2 implementation
# Open and review code apps/web-clipper/background.js:302-326 -
Plan the implementation
- What data do we need? (tab URLs, titles)
- Where does the code go? (background service worker)
- What messages are needed? (none - initiated by context menu)
- What UI changes? (add context menu item)
-
Ask Copilot for guidance (Chat Pane - free)
Looking at the "save tabs" feature in apps/web-clipper/background.js:302-326, what's the best approach for MV3? I need to: - Get all open tabs - Create a single note with links to all tabs - Handle errors gracefully See docs/MIGRATION-PATTERNS.md for our coding patterns. -
Implement using Agent Mode (uses task)
Implement "save tabs" feature from FEATURE-PARITY-CHECKLIST.md. Legacy reference: apps/web-clipper/background.js:302-326 Files to modify: - src/background/index.ts (add context menu + handler) - manifest.json (verify permissions) Use Pattern 5 (context menu) and Pattern 8 (Trilium API) from docs/MIGRATION-PATTERNS.md. Update FEATURE-PARITY-CHECKLIST.md when done. -
Fix TypeScript errors (Inline Chat - free)
- Press Ctrl+I on error
- Copilot suggests fix
- Accept or modify
-
Test manually
- Open multiple tabs
- Right-click → "Save Tabs to Trilium"
- Check Trilium for new note
- Verify all links present
-
Update documentation
- Mark feature complete in
FEATURE-PARITY-CHECKLIST.md - Commit changes
- Mark feature complete in
Task 2: Fix a Bug
Example: Screenshot not being cropped
Steps:
-
Reproduce the bug
- Take screenshot with selection
- Save to Trilium
- Check if image is cropped or full-page
-
Check the logs
- Open extension popup → Logs button
- Filter by "screenshot" or "crop"
- Look for errors or unexpected values
-
Locate the code
# Search for relevant functions rg "captureScreenshot" src/ rg "cropImage" src/ -
Review the legacy implementation
code apps/web-clipper/background.js:393-427 # MV2 crop function -
Ask Copilot for analysis (Chat Pane - free)
In src/background/index.ts around line 504-560, we capture screenshots but don't apply the crop rectangle. The crop rect is stored in metadata but the image is still full-page. MV2 implementation is in apps/web-clipper/background.js:393-427. What's the best way to implement cropping in MV3 using OffscreenCanvas? -
Implement the fix (Agent Mode - uses task)
Fix screenshot cropping in src/background/index.ts. Problem: Crop rectangle stored but not applied to image. Reference: apps/web-clipper/background.js:393-427 for logic Solution: Use OffscreenCanvas to crop before saving Use Pattern 3 from docs/MIGRATION-PATTERNS.md. Update FEATURE-PARITY-CHECKLIST.md when fixed. -
Test thoroughly
- Small crop (100x100)
- Large crop (full page)
- Edge crops (near borders)
- Very tall/wide crops
-
Verify logs show success
- Check Logs page for crop dimensions
- Verify no errors
Task 3: Add UI Component with Theme Support
Example: Adding a "Recent Notes" section to popup
Steps:
-
Plan the UI
- Sketch layout on paper
- Identify needed data (recent note IDs, titles)
- Plan data flow (background ↔ popup)
-
Update HTML (
src/popup/popup.html)<div class="recent-notes"> <h3>Recently Saved</h3> <ul id="recent-list"></ul> </div> -
Add CSS with theme variables (
src/popup/popup.css)@import url('../shared/theme.css'); /* Critical */ .recent-notes { background: var(--color-surface-elevated); border: 1px solid var(--color-border); padding: 12px; border-radius: 8px; } .recent-notes h3 { color: var(--color-text-primary); margin: 0 0 8px 0; } #recent-list { list-style: none; padding: 0; margin: 0; } #recent-list li { color: var(--color-text-secondary); padding: 4px 0; border-bottom: 1px solid var(--color-border-subtle); } #recent-list li:last-child { border-bottom: none; } #recent-list li a { color: var(--color-primary); text-decoration: none; } #recent-list li a:hover { color: var(--color-primary-hover); } -
Add TypeScript logic (
src/popup/index.ts)import { Logger } from '@/shared/utils'; import { ThemeManager } from '@/shared/theme'; const logger = Logger.create('RecentNotes', 'popup'); async function loadRecentNotes(): Promise<void> { try { const { recentNotes } = await chrome.storage.local.get(['recentNotes']); const list = document.getElementById('recent-list'); if (!list || !recentNotes || recentNotes.length === 0) { list.innerHTML = '<li>No recent notes</li>'; return; } list.innerHTML = recentNotes .slice(0, 5) // Show 5 most recent .map(note => ` <li> <a href="${note.url}" target="_blank"> ${escapeHtml(note.title)} </a> </li> `) .join(''); logger.debug('Recent notes loaded', { count: recentNotes.length }); } catch (error) { logger.error('Failed to load recent notes', error); } } function escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize when popup opens document.addEventListener('DOMContentLoaded', async () => { await ThemeManager.initialize(); await loadRecentNotes(); }); -
Store recent notes when saving (
src/background/index.ts)async function addToRecentNotes(noteId: string, title: string, url: string): Promise<void> { try { const { recentNotes = [] } = await chrome.storage.local.get(['recentNotes']); // Add to front, remove duplicates, limit to 10 const updated = [ { noteId, title, url: `${triliumUrl}/#${noteId}`, timestamp: Date.now() }, ...recentNotes.filter(n => n.noteId !== noteId) ].slice(0, 10); await chrome.storage.local.set({ recentNotes: updated }); logger.debug('Added to recent notes', { noteId, title }); } catch (error) { logger.error('Failed to update recent notes', error); } } -
Test theme switching
- Open popup
- Toggle theme (sun/moon icon)
- Verify colors change immediately
- Check both light and dark modes
Task 4: Debug Service Worker Issues
Problem: Service worker terminating unexpectedly or not receiving messages
Debugging Steps:
-
Check service worker status
chrome://extensions/ → Find extension → "Service worker" link (should say "active") -
Open service worker console
- Click "Service worker" link
- Console opens in new window
- Check for errors on load
-
Test message passing
- Add temporary logging in content script:
logger.info('Sending message to background'); chrome.runtime.sendMessage({ type: 'TEST' }, (response) => { logger.info('Response received', response); });- Check both consoles for logs
-
Check storage persistence
// In background chrome.runtime.onInstalled.addListener(async () => { logger.info('Service worker installed'); const data = await chrome.storage.local.get(); logger.debug('Stored data', data); }); -
Monitor service worker lifecycle
- Watch "Service worker" status on extensions page
- Should stay "active" when doing work
- May say "inactive" when idle (normal)
- If it says "stopped" or errors, check console
-
Common fixes:
- Ensure message handlers return
truefor async - Don't use global variables for state
- Use
chrome.storagefor persistence - Check for syntax errors (TypeScript)
- Ensure message handlers return
Task 5: Test in Different Scenarios
Coverage checklist:
Content Types
- Simple article (blog post, news)
- Image-heavy page (gallery, Pinterest)
- Code documentation (GitHub, Stack Overflow)
- Social media (Twitter thread, LinkedIn post)
- Video page (YouTube, Vimeo)
- Dynamic SPA (React/Vue app)
Network Conditions
- Fast network
- Slow network (throttle in DevTools)
- Offline (service worker should handle gracefully)
- Trilium server down
Edge Cases
- Very long page (20+ screens)
- Page with 100+ images
- Page with no title
- Page with special characters in title
- Restricted URL (chrome://, about:, file://)
- Page with large selection (5000+ words)
Browser States
- Fresh install
- After settings change
- After theme toggle
- After browser restart
- Multiple tabs open simultaneously
Debugging Checklist
When something doesn't work:
1. Check Build
# Any errors during build?
npm run build
# TypeScript errors?
npm run type-check
2. Check Extension Status
- Extension loaded in Chrome?
- Extension enabled?
- Correct dist/ folder selected?
- Service worker "active"?
3. Check Consoles
- Service worker console (no errors?)
- Popup console (if UI issue)
- Page console (if content script issue)
- Extension logs page
4. Check Permissions
- Required permissions in manifest.json?
- Host permissions for Trilium URL?
- User granted permissions?
5. Check Storage
// In any context console
chrome.storage.local.get(null, (data) => console.log(data));
chrome.storage.sync.get(null, (data) => console.log(data));
6. Check Network
- Trilium server reachable?
- Auth token valid?
- CORS headers correct?
- Network tab in DevTools
Performance Tips
Keep Service Worker Fast
- Minimize work in message handlers
- Use
chrome.alarmsfor scheduled tasks - Offload heavy processing to content scripts when possible
Optimize Content Scripts
- Inject only when needed (use
activeTabpermission) - Remove listeners when done
- Don't poll DOM excessively
Storage Best Practices
- Use
chrome.storage.localfor large data - Use
chrome.storage.syncfor small settings only - Clear old data periodically
- Batch storage operations
Code Quality Checklist
Before committing:
npm run type-checkpasses- No console errors in any context
- Centralized logging used throughout
- Theme system integrated (if UI)
- Error handling on all async operations
- No hardcoded colors (use CSS variables)
- No emojis in code
- Comments explain "why", not "what"
- Updated FEATURE-PARITY-CHECKLIST.md
- Tested manually
Git Workflow
Commit Messages
# Feature
git commit -m "feat: add save tabs functionality"
# Bug fix
git commit -m "fix: screenshot cropping now works correctly"
# Docs
git commit -m "docs: update feature checklist"
# Refactor
git commit -m "refactor: extract image processing to separate function"
Before Pull Request
- Ensure all features from current phase complete
- Run full test suite manually
- Update all documentation
- Clean commit history (squash if needed)
- Write comprehensive PR description
Troubleshooting Guide
Issue: Extension won't load
Symptoms: Error on chrome://extensions/ page
Solutions:
# 1. Check manifest is valid
cat dist/manifest.json | jq . # Should parse without errors
# 2. Rebuild from scratch
npm run clean
npm run build
# 3. Check for syntax errors
npm run type-check
# 4. Verify all referenced files exist
ls dist/background.js dist/content.js dist/popup.html
Issue: Content script not injecting
Symptoms: No toast, no selection detection, no overlay
Solutions:
- Check URL isn't restricted (chrome://, about:, file://)
- Check manifest
content_scripts.matchespatterns - Verify extension has permission for the site
- Check content.js exists in dist/
- Look for errors in page console (F12)
Issue: Buttons in popup don't work
Symptoms: Clicking buttons does nothing
Solutions:
- Right-click popup → Inspect
- Check console for JavaScript errors
- Verify event listeners attached:
// In popup/index.ts, check DOMContentLoaded fired logger.info('Popup initialized'); - Check if popup.js loaded:
<!-- In dist/popup.html, verify: --> <script src="popup.js"></script>
Issue: Theme not working
Symptoms: Always light mode, or styles broken
Solutions:
- Check theme.css imported:
/* At top of CSS file */ @import url('../shared/theme.css'); - Check ThemeManager initialized:
await ThemeManager.initialize(); - Verify CSS variables used:
/* NOT: color: #333; */ color: var(--color-text-primary); /* YES */ - Check chrome.storage has theme data:
chrome.storage.sync.get(['theme'], (data) => console.log(data));
Issue: Can't connect to Trilium
Symptoms: "Connection failed" or network errors
Solutions:
- Test URL in browser directly
- Check CORS headers on Trilium server
- Verify auth token format (should be long string)
- Check host_permissions in manifest includes Trilium URL
- Test with curl:
curl -H "Authorization: YOUR_TOKEN" https://trilium.example.com/api/notes
Issue: Logs not showing
Symptoms: Empty logs page or missing entries
Solutions:
- Check centralized logging initialized:
const logger = Logger.create('ComponentName', 'background'); logger.info('Test message'); // Should appear in logs - Check storage has logs:
chrome.storage.local.get(['centralizedLogs'], (data) => { console.log(data.centralizedLogs?.length || 0, 'logs'); }); - Clear and regenerate logs:
chrome.storage.local.remove(['centralizedLogs']); // Then perform actions to generate new logs
Issue: Service worker keeps stopping
Symptoms: "Service worker (stopped)" on extensions page
Solutions:
- Check for unhandled promise rejections:
// Add to all async functions try { await someOperation(); } catch (error) { logger.error('Operation failed', error); // Don't let error propagate unhandled } - Ensure message handlers return boolean:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { handleMessageAsync(msg, sender, sendResponse); return true; // CRITICAL }); - Check for syntax errors that crash on load:
npm run type-check
Quick Command Reference
Development
# Start dev build (watch mode)
npm run dev
# Type check (watch mode)
npm run type-check --watch
# Clean build artifacts
npm run clean
# Full rebuild
npm run clean && npm run build
# Format code
npm run format
# Lint code
npm run lint
Chrome Commands
// In any console
// View all storage
chrome.storage.local.get(null, console.log);
chrome.storage.sync.get(null, console.log);
// Clear storage
chrome.storage.local.clear();
chrome.storage.sync.clear();
// Check runtime info
chrome.runtime.getManifest();
chrome.runtime.id;
// Get extension version
chrome.runtime.getManifest().version;
Debugging Shortcuts
// Temporary debug logging
const DEBUG = true;
if (DEBUG) logger.debug('Debug info', { data });
// Quick performance check
console.time('operation');
await longRunningOperation();
console.timeEnd('operation');
// Inspect object
console.dir(complexObject, { depth: null });
// Trace function calls
console.trace('Function called');
VS Code Tips
Essential Extensions
- GitHub Copilot: AI pair programming
- ESLint: Code quality
- Prettier: Code formatting
- Error Lens: Inline error display
- TypeScript Vue Plugin: Enhanced TS support
Keyboard Shortcuts
Ctrl+Shift+P: Command paletteCtrl+P: Quick file openCtrl+B: Toggle sidebar- `Ctrl+``: Toggle terminal
Ctrl+Shift+F: Find in filesCtrl+I: Inline Copilot chatCtrl+Alt+I: Copilot chat pane
Useful Copilot Prompts
# Quick explanation
/explain What does this function do?
# Generate tests
/tests Generate test cases for this function
# Fix issues
/fix Fix the TypeScript errors in this file
# Optimize
/optimize Make this function more efficient
Custom Snippets
Add to .vscode/snippets.code-snippets:
{
"Logger Import": {
"prefix": "log-import",
"body": [
"import { Logger } from '@/shared/utils';",
"const logger = Logger.create('$1', '$2');"
]
},
"Try-Catch Block": {
"prefix": "try-log",
"body": [
"try {",
" $1",
"} catch (error) {",
" logger.error('$2', error);",
" throw error;",
"}"
]
},
"Message Handler": {
"prefix": "msg-handler",
"body": [
"chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {",
" (async () => {",
" try {",
" const result = await handle$1(message);",
" sendResponse({ success: true, data: result });",
" } catch (error) {",
" logger.error('$1 handler error', error);",
" sendResponse({ success: false, error: error.message });",
" }",
" })();",
" return true;",
"});"
]
}
}
Architecture Decision Log
Keep track of important decisions:
Decision 1: Use IIFE Build Format
Date: October 2025
Reason: Simpler than ES modules for Chrome extensions, better browser compatibility
Trade-off: No dynamic imports, larger bundle size
Decision 2: Centralized Logging System
Date: October 2025
Reason: Service workers terminate frequently, console.log doesn't persist
Trade-off: Small overhead, but massive debugging improvement
Decision 3: OffscreenCanvas for Screenshots
Date: October 2025 (planned)
Reason: Service workers can't access DOM canvas
Trade-off: More complex API, but necessary for MV3
Decision 4: Store Recent Notes in Local Storage
Date: October 2025 (planned)
Reason: Faster access, doesn't need to sync across devices
Trade-off: Won't sync, but not critical for this feature
Performance Benchmarks
Track performance as you develop:
Screenshot Capture (Target)
- Full page capture: < 500ms
- Crop operation: < 100ms
- Total save time: < 2s
Content Processing (Target)
- Readability extraction: < 300ms
- DOMPurify sanitization: < 200ms
- Cheerio cleanup: < 100ms
- Image processing (10 images): < 3s
Storage Operations (Target)
- Save settings: < 50ms
- Load settings: < 50ms
- Add log entry: < 20ms
How to measure:
const start = performance.now();
await someOperation();
const duration = performance.now() - start;
logger.info('Operation completed', { duration });
Testing Scenarios
Scenario 1: New User First-Time Setup
- Install extension
- Open popup
- Click "Configure Trilium"
- Enter server URL and token
- Test connection
- Save settings
- Try to save a page
- Verify note created in Trilium
Expected: Smooth onboarding, clear error messages if something fails
Scenario 2: Network Interruption
- Start saving a page
- Disconnect network mid-save
- Check error handling
- Reconnect network
- Retry save
Expected: Graceful error, no crashes, clear user feedback
Scenario 3: Service Worker Restart
- Trigger service worker to sleep (wait 30s idle)
- Perform action that wakes it (open popup)
- Check if state persisted correctly
- Verify functionality still works
Expected: Seamless experience, user doesn't notice restart
Scenario 4: Theme Switching
- Open popup in light mode
- Toggle to dark mode
- Close popup
- Reopen popup
- Verify dark mode persisted
- Change system theme
- Set extension to "System"
- Verify it follows system theme
Expected: Instant visual feedback, persistent preference
Code Review Checklist
Before asking for PR review:
Functionality
- Feature works as intended
- Edge cases handled
- Error messages are helpful
- No console errors/warnings
Code Quality
- TypeScript with no
anytypes - Centralized logging used
- Theme system integrated (if UI)
- No hardcoded values (use constants)
- Functions are single-purpose
- No duplicate code
Documentation
- Code comments explain "why", not "what"
- Complex logic has explanatory comments
- FEATURE-PARITY-CHECKLIST.md updated
- README updated if needed
Testing
- Manually tested all paths
- Tested error scenarios
- Tested on different page types
- Checked performance
Git
- Meaningful commit messages
- Commits are logical units
- No debug code committed
- No commented-out code
Resources
Chrome Extension Docs (Local)
reference/chrome_extension_docs/- Manifest V3 API reference
Library Docs (Local)
reference/Mozilla_Readability_docs/- Content extractionreference/cure53_DOMPurify_docs/- HTML sanitizationreference/cheerio_docs/- DOM manipulation
External Links
Community
Last Updated: October 18, 2025
Maintainer: Development team
Quick Links: