Back
Lesson 11

Working with Custom Pages

View Source Code

Overview

Create full-page interfaces for your extension using custom HTML pages. This lesson builds a notes management page listing all saved notes, demonstrating how to create standalone extension pages beyond popups.

By the end, TabNotes will have a dedicated page showing all notes with links to their corresponding websites.

What You’ll Learn

  • Create custom extension pages in WXT
  • Build unlisted pages (accessed via URL, not browser UI)
  • Access extension storage from custom pages
  • Display and format stored data
  • Link to extension pages from popups
  • Handle empty states and error conditions

Extension Page Types

Extensions support several page types beyond popups:

Page TypeManifest KeyUse Case
Popupaction.default_popupQuick actions, minimal UI
Optionsoptions_pageExtension settings
DevToolsdevtools_pageBrowser DevTools panels
New Tabchrome_url_overrides.newtabReplace new tab page
UnlistedNoneCustom pages accessed by URL

For TabNotes: Create an unlisted page for viewing all notes.


Creating Unlisted Pages in WXT

Unlisted pages don’t appear in browser UI - users access them via direct URL.

Step 1: Create the Page File

entrypoints/
└── notes-list.html

WXT automatically makes this page accessible at:

chrome-extension://[extension-id]/notes-list.html

Step 2: Build the HTML Structure

<!-- entrypoints/notes-list.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>All Notes - TabNotes</title>
</head>
<body>
  <div id="app">
    <h1>All Your Notes</h1>
    <div id="notes-container"></div>
  </div>
  <script type="module" src="./notes-list.ts"></script>
</body>
</html>

Step 3: Create the Script File

// entrypoints/notes-list.ts
import './notes-list.css';
import { storage } from 'wxt/storage';

document.addEventListener('DOMContentLoaded', async () => {
  const container = document.getElementById('notes-container');
  
  // Load all notes
  const notesStorage = await storage.getItem('sync:notesStorage') || {};
  
  // Render notes
  if (Object.keys(notesStorage).length === 0) {
    container.innerHTML = '<p>No notes yet. Start browsing and take notes!</p>';
  } else {
    container.innerHTML = Object.entries(notesStorage)
      .map(([pageKey, noteData]) => `
        <div class="note-card">
          <h3>${pageKey}</h3>
          <p>${noteData.note}</p>
          <small>Updated: ${new Date(noteData.updatedAt).toLocaleDateString()}</small>
        </div>
      `)
      .join('');
  }
});

Styling the Notes List

/* entrypoints/notes-list.css */
body {
  font-family: system-ui, -apple-system, sans-serif;
  margin: 0;
  padding: 20px;
  background-color: #f5f5f5;
}

#app {
  max-width: 800px;
  margin: 0 auto;
}

h1 {
  color: #333;
  margin-bottom: 30px;
}

.note-card {
  background: white;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 15px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.note-card h3 {
  margin: 0 0 10px 0;
  color: #444;
  font-size: 16px;
}

.note-card p {
  margin: 0 0 10px 0;
  color: #666;
  line-height: 1.5;
}

.note-card small {
  color: #999;
  font-size: 12px;
}

Linking to Custom Pages

From Popup

Add a button in the popup to open the notes list:

// entrypoints/popup/main.ts
const viewAllButton = document.createElement('button');
viewAllButton.textContent = 'View All Notes';
viewAllButton.addEventListener('click', () => {
  browser.tabs.create({
    url: browser.runtime.getURL('notes-list.html')
  });
});

document.querySelector('#app').appendChild(viewAllButton);

browser.runtime.getURL() - Converts relative path to full extension URL.

Opening in Current vs New Tab

// Open in new tab
browser.tabs.create({ url: extensionURL });

// Open in current tab
browser.tabs.update({ url: extensionURL });

Displaying Note Metadata

Enhance notes display with more information:

// entrypoints/notes-list.ts
const formatNote = (pageKey: string, noteData: any) => {
  const createdDate = new Date(noteData.createdAt).toLocaleString();
  const updatedDate = new Date(noteData.updatedAt).toLocaleString();
  const wordCount = noteData.note.split(/\s+/).length;
  
  return `
    <div class="note-card">
      <div class="note-header">
        <h3>${pageKey}</h3>
        <a href="https://${pageKey}" target="_blank">Visit Page</a>
      </div>
      <p class="note-content">${noteData.note}</p>
      <div class="note-meta">
        <span>${wordCount} words</span>
        <span>Created: ${createdDate}</span>
        <span>Updated: ${updatedDate}</span>
      </div>
    </div>
  `;
};

Adding Delete Functionality

Allow users to delete notes from the list:

// entrypoints/notes-list.ts
const deleteNote = async (pageKey: string) => {
  const notesStorage = await storage.getItem('sync:notesStorage') || {};
  delete notesStorage[pageKey];
  await storage.setItem('sync:notesStorage', notesStorage);
  
  // Reload page to reflect changes
  window.location.reload();
};

// In render function
const formatNote = (pageKey: string, noteData: any) => {
  return `
    <div class="note-card">
      <h3>${pageKey}</h3>
      <p>${noteData.note}</p>
      <button class="delete-btn" data-page-key="${pageKey}">Delete</button>
    </div>
  `;
};

// Add event listeners after rendering
document.querySelectorAll('.delete-btn').forEach(btn => {
  btn.addEventListener('click', (e) => {
    const pageKey = (e.target as HTMLElement).dataset.pageKey;
    if (confirm('Delete this note?')) {
      deleteNote(pageKey);
    }
  });
});

Search and Filter

Add search functionality:

// entrypoints/notes-list.ts
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search notes...';
searchInput.addEventListener('input', (e) => {
  const query = (e.target as HTMLInputElement).value.toLowerCase();
  filterNotes(query);
});

const filterNotes = (query: string) => {
  const cards = document.querySelectorAll('.note-card');
  cards.forEach(card => {
    const text = card.textContent?.toLowerCase() || '';
    card.style.display = text.includes(query) ? 'block' : 'none';
  });
};

Options Pages vs Unlisted Pages

Options Pages

Accessible via chrome://extensions → Extension Details → “Extension options”:

// entrypoints/options.html
// Automatically linked in manifest as options_page

Use For: Extension settings, preferences

Unlisted Pages

No automatic browser UI:

// entrypoints/custom-page.html
// Accessible only via direct URL

Use For: Feature pages, dashboards, data views

TabNotes uses unlisted page - notes list is a feature, not settings.


Debugging Custom Pages

Opening DevTools

  1. Navigate to your custom page
  2. Right-click → Inspect
  3. DevTools opens like normal webpage

Common Issues

Page Not Found (404):

  • Check file exists in entrypoints/
  • Verify URL: browser.runtime.getURL('filename.html')
  • Restart dev server

Storage Empty:

// Add debug logging
const notesStorage = await storage.getItem('sync:notesStorage');
console.log('Loaded storage:', notesStorage);

What’s Next

TabNotes now has a dedicated notes management page where users can view all saved notes in one place. The extension is feature-complete from a functionality standpoint.

Lesson 12 covers extension lifecycle and service worker best practices - understanding how background scripts start, stop, and handle state management.