Build a Pinterest Notes Chrome Extension

Salo By Salo 5 min read
Build a Pinterest Notes Chrome Extension

This tutorial builds the smallest useful Chrome Manifest V3 prototype: a private textarea mounted on Pinterest Pin pages, keyed by the numeric Pin ID, and saved in chrome.storage.local. It is intentionally a prototype, not a production-ready clone of Pinterest's former notes.

Pinterest's DOM can change without notice. Keep selectors isolated, avoid imitating Pinterest's protected branding, and test across the page types and locales you support.

1. Create the Manifest V3 extension

Create a folder with manifest.json and content.js. Chrome's manifest reference is the source of truth for available keys.

{
  "manifest_version": 3,
  "name": "Pin Context Prototype",
  "version": "0.1.0",
  "description": "Save a private local note for a Pinterest Pin.",
  "permissions": ["storage"],
  "content_scripts": [{
    "matches": ["https://*.pinterest.com/*"],
    "js": ["content.js"],
    "run_at": "document_idle"
  }]
}

The host match is broad because Pinterest uses client-side navigation. If your feature supports fewer routes or country domains, narrow and document that scope.

2. Derive a stable key from the Pin URL

Do not key notes by a card's position or visible title. Extract the numeric ID from a canonical /pin/…/ path and return null everywhere else.

function getPinId(url = location.href) {
  try {
    const { pathname } = new URL(url);
    const match = pathname.match(//pin/(d+)/?/);
    return match ? match[1] : null;
  } catch {
    return null;
  }
}

Test valid detail URLs, query strings, non-Pin pages, malformed URLs, and any locale patterns you claim to support. Never silently attach a note when the ID is uncertain.

3. Store a versioned record

Keep the schema explicit so future migrations are possible:

async function readNote(pinId) {
  const key = 'pin-note:' + pinId;
  const result = await chrome.storage.local.get(key);
  return result[key] ?? { version: 1, pinId, text: '', updatedAt: null };
}

async function writeNote(pinId, text) {
  const key = 'pin-note:' + pinId;
  const record = {
    version: 1,
    pinId,
    text,
    updatedAt: new Date().toISOString()
  };
  await chrome.storage.local.set({ [key]: record });
  return record;
}

Chrome documents a default 10 MB limit for storage.local, with a separate unlimitedStorage permission for increasing it. Chrome also documents that local extension storage is cleared when the extension is removed. Read the current Storage API documentation and design export before inviting important data.

4. Mount an isolated, accessible editor

A Shadow DOM reduces accidental style collisions. The example appends to the page body so it does not depend on a brittle Pinterest class name:

async function mountEditor(pinId) {
  if (document.querySelector('[data-pin-context-root]')) return;

  const host = document.createElement('aside');
  host.dataset.pinContextRoot = '';
  host.setAttribute('aria-label', 'Private Pin note');
  document.body.append(host);

  const root = host.attachShadow({ mode: 'open' });
  const label = document.createElement('label');
  label.textContent = 'Private note';
  const editor = document.createElement('textarea');
  editor.setAttribute('aria-label', 'Private note for this Pin');
  const status = document.createElement('span');
  status.setAttribute('role', 'status');

  const saved = await readNote(pinId);
  editor.value = saved.text;
  root.append(label, editor, status);
  bindAutosave(editor, status, pinId);
}

Create interface text with textContent, not untrusted innerHTML. Add visible focus styles, sufficient contrast, a real label, keyboard access, and status text that does not steal focus.

5. Save without writing on every keystroke

function bindAutosave(editor, status, pinId) {
  let saveTimer;
  editor.addEventListener('input', () => {
    status.textContent = 'Unsaved';
    clearTimeout(saveTimer);
    saveTimer = setTimeout(async () => {
      try {
        await writeNote(pinId, editor.value);
        status.textContent = 'Saved locally';
      } catch (error) {
        console.error('Pin note save failed', error);
        status.textContent = 'Could not save';
      }
    }, 400);
  });
}

For production, also flush pending input on page transition, protect against two tabs overwriting each other, define a maximum note size, and surface quota errors.

6. Handle Pinterest's client-side navigation

A one-time page-load handler misses navigation between feed and detail views. Use an idempotent reconciliation function and observe changes without remounting on every mutation:

let scheduled = false;

async function reconcile() {
  scheduled = false;
  const pinId = getPinId();
  const existing = document.querySelector('[data-pin-context-root]');
  if (!pinId) {
    existing?.remove();
    return;
  }
  if (existing?.dataset.pinId === pinId) return;
  existing?.remove();
  await mountEditor(pinId);
  const mounted = document.querySelector('[data-pin-context-root]');
  if (mounted) mounted.dataset.pinId = pinId;
}

new MutationObserver(() => {
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(reconcile);
}).observe(document.documentElement, { childList: true, subtree: true });

reconcile();

A production version should also watch navigation state directly where possible and disconnect observers it no longer needs. Profile the code on long feeds; a correct feature that degrades page performance is still a bad extension.

7. Respect Manifest V3 security constraints

Manifest V3 prohibits remotely hosted executable code. Bundle executable JavaScript with the extension, keep the default content security policy unless you have a justified change, avoid dynamic code execution, and request only the permissions the feature requires. Chrome's Manifest V3 overview explains the platform model.

8. Add the features a real product needs

9. Test the failure paths

  1. Load the unpacked extension from chrome://extensions.
  2. Visit a feed, a board, a Pin detail page, and a non-Pin route.
  3. Navigate between two Pins without a reload and confirm note isolation.
  4. Open the same Pin in two tabs and test conflict behavior.
  5. Disable storage or simulate quota failure and verify the warning.
  6. Export, remove a test record, and restore it.
  7. Test keyboard navigation, zoom, screen-reader labels, and dark/light page conditions.

The engineering lesson

The textarea is the easy part. A trustworthy Pinterest extension depends on stable identity, resilient lifecycle handling, accessible UI, constrained permissions, documented storage, and a tested exit path. Build those qualities before expanding the feature list.

Keep building your Pinterest system

Continue with the guide that matches your next step:

Salo

About the Author

Salo is a product designer and power user who writes about digital organization, Pinterest workflows, and tools for better thinking.

Get the Notestopin Chrome extension

Add private notes to any Pin, tag them, and search your saves later.

Add to Chrome