Claude Skills Guide

Chrome Extension Read Later List: A Developer Guide

Building a Chrome extension to manage a read later list is one of the most practical projects you can undertake as a developer. It touches on several Chrome extension APIs—storage, messaging, context menus, and browser action popups—while solving a real problem that users face daily. This guide walks you through the architecture and implementation details needed to create a functional read later list extension.

Understanding the Core Requirements

A read later list extension needs to accomplish three primary tasks: capture URLs from the current tab, store them persistently, and retrieve them for later viewing. Beyond these basics, you’ll want features like tagging, search, and offline access. The Chrome platform provides all the building blocks you need.

The extension architecture typically consists of a popup interface for quick interactions, a background script for handling messages and sync operations, and a options page for managing settings. Understanding how these components communicate is essential before writing any code.

Storage Options in Chrome Extensions

Chrome provides three primary storage mechanisms for extensions. Each has distinct characteristics that make it suitable for different use cases.

chrome.storage.local stores data locally on the user’s machine with no synchronization. It offers up to 5MB of storage by default, which can be increased by requesting the unlimitedStorage permission. This is the simplest option and works well for single-device scenarios.

// Saving a URL to local storage
async function saveForLater(tab) {
  const item = {
    id: Date.now().toString(),
    url: tab.url,
    title: tab.title,
    timestamp: new Date().toISOString(),
    favicon: tab.favIconUrl
  };

  const result = await chrome.storage.local.get(['readLaterList']);
  const list = result.readLaterList || [];
  list.unshift(item); // Add to beginning of list
  
  await chrome.storage.local.set({ readLaterList: list });
  return item;
}

chrome.storage.sync synchronizes data across all devices where the user is signed into Chrome. It provides automatic cloud backup but has stricter quota limits—only 100KB total. This is ideal for users who want their reading list available everywhere.

chrome.storage.session stores data only for the current browser session. This is useful for temporary state but inappropriate for a read later list that needs persistence across browser restarts.

For most read later extensions, a hybrid approach works best: store the actual list items in local storage for capacity, and sync only lightweight metadata or preferences using sync storage.

Building the Popup Interface

The popup is what users interact with most frequently. It should load quickly and provide immediate access to saved articles. Here’s a practical implementation pattern:

// popup.js - Loading and displaying the list
document.addEventListener('DOMContentLoaded', async () => {
  const result = await chrome.storage.local.get(['readLaterList']);
  const list = result.readLaterList || [];
  
  const container = document.getElementById('article-list');
  
  if (list.length === 0) {
    container.innerHTML = '<p class="empty-state">No articles saved yet</p>';
    return;
  }
  
  container.innerHTML = list.map(item => `
    <div class="article-item" data-id="${item.id}">
      <img src="${item.favicon || 'icon.png'}" class="favicon" alt="">
      <div class="article-info">
        <a href="${item.url}" target="_blank" class="article-title">${item.title}</a>
        <span class="article-domain">${new URL(item.url).hostname}</span>
      </div>
      <button class="delete-btn" data-id="${item.id}">×</button>
    </div>
  `).join('');
  
  // Handle delete actions
  container.querySelectorAll('.delete-btn').forEach(btn => {
    btn.addEventListener('click', async (e) => {
      const id = e.target.dataset.id;
      await removeArticle(id);
      location.reload();
    });
  });
});

async function removeArticle(id) {
  const result = await chrome.storage.local.get(['readLaterList']);
  const list = result.readLaterList || [];
  const updated = list.filter(item => item.id !== id);
  await chrome.storage.local.set({ readLaterList: updated });
}

The popup should also include a button to save the current tab. This requires communicating with the background script or directly querying the active tab:

// Add current page to reading list
document.getElementById('save-btn').addEventListener('click', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  await saveForLater(tab);
  document.getElementById('save-btn').textContent = 'Saved!';
  setTimeout(() => {
    document.getElementById('save-btn').textContent = 'Save for Later';
  }, 2000);
});

Context Menu Integration

Adding a context menu option lets users save pages without opening your popup. This significantly increases engagement since it requires fewer clicks. Here’s how to implement it:

// background.js
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: 'saveToReadLater',
    title: 'Save to Read Later List',
    contexts: ['page', 'link']
  });
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId === 'saveToReadLater') {
    const url = info.linkUrl || info.pageUrl;
    
    // Fetch page title if saving a link
    const title = info.linkUrl 
      ? await fetchPageTitle(info.linkUrl)
      : tab.title;
    
    const item = {
      id: Date.now().toString(),
      url: url,
      title: title || 'Untitled',
      timestamp: new Date().toISOString(),
      favicon: tab.favIconUrl
    };
    
    const result = await chrome.storage.local.get(['readLaterList']);
    const list = result.readLaterList || [];
    list.unshift(item);
    await chrome.storage.local.set({ readLaterList: list });
    
    // Show notification
    chrome.notifications.create({
      type: 'basic',
      iconUrl: 'icon.png',
      title: 'Saved!',
      message: `"${title}" added to your reading list`
    });
  }
});

async function fetchPageTitle(url) {
  try {
    const response = await fetch(url);
    const text = await response.text();
    const match = text.match(/<title[^>]*>([^<]+)<\/title>/i);
    return match ? match[1].trim() : null;
  } catch {
    return null;
  }
}

Adding Search and Organization

As the list grows, users need ways to find specific articles. A simple search filter can be implemented entirely in the popup:

// popup.js - Search functionality
document.getElementById('search-input').addEventListener('input', (e) => {
  const query = e.target.value.toLowerCase();
  const items = document.querySelectorAll('.article-item');
  
  items.forEach(item => {
    const title = item.querySelector('.article-title').textContent.toLowerCase();
    const domain = item.querySelector('.article-domain').textContent.toLowerCase();
    const visible = title.includes(query) || domain.includes(query);
    item.style.display = visible ? 'flex' : 'none';
  });
});

For tagging and categorization, you’ll need to modify your data structure to include an array of tags and add UI elements for assigning them:

// Updated data structure with tags
const item = {
  id: Date.now().toString(),
  url: tab.url,
  title: tab.title,
  timestamp: new Date().toISOString(),
  tags: ['tech', 'javascript'], // User-assigned tags
  read: false
};

Background Sync for Cross-Device Access

If you want users to access their reading list across multiple devices, you’ll need to implement sync logic. The simplest approach uses chrome.storage.sync:

// background.js - Periodic sync
chrome.alarms.create('syncReadLater', { periodInMinutes: 15 });

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'syncReadLater') {
    const local = await chrome.storage.local.get(['readLaterList']);
    const sync = await chrome.storage.sync.get(['readLaterList']);
    
    // Merge lists, keeping most recent items
    const merged = mergeLists(local.readLaterList || [], sync.readLaterList || []);
    
    await chrome.storage.local.set({ readLaterList: merged });
    await chrome.storage.sync.set({ readLaterList: merged });
  }
});

function mergeLists(local, sync) {
  const combined = [...local, ...sync];
  const unique = new Map();
  
  combined.forEach(item => {
    if (!unique.has(item.id) || 
        new Date(item.timestamp) > new Date(unique.get(item.id).timestamp)) {
      unique.set(item.id, item);
    }
  });
  
  return Array.from(unique.values())
    .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
}

Extension Manifest Configuration

Finally, your manifest.json needs the correct permissions:

{
  "manifest_version": 3,
  "name": "Read Later List",
  "version": "1.0",
  "permissions": [
    "storage",
    "tabs",
    "contextMenus",
    "notifications",
    "alarms"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon.png"
  },
  "background": {
    "service_worker": "background.js"
  }
}

Building a production-ready read later extension involves handling edge cases like duplicate URLs, managing storage quotas, and creating a polished UI. The patterns shown here provide a solid foundation that you can extend based on your specific requirements.

The key to a successful implementation is starting simple—save URLs and display them—then iteratively adding features like search, tagging, and sync. This approach lets you validate user needs before investing time in more complex functionality.

Built by theluckystrike — More at zovo.one