Kamu sedang menulis README atau membalas issue di GitHub, lalu perlu menempel link ke suatu halaman. Alurnya biasanya begini: seleksi judul di tab, klik kanan, pilih Salin, pindah ke editor, ketik kurung siku, tempel judul, ketik kurung tutup, ketik kurung buka, tempel URL, ketik kurung tutup. Hampir sepuluh langkah demi satu baris Markdown. Saya menulis link semacam ini puluhan kali sehari, jadi saya bikin Chrome extension kecil yang memangkas semuanya jadi satu klik.

Yang Akan Kamu Bangun

Extension ini bernama Markdown Link Copier. Kamu klik ikonnya di toolbar, muncul popup berisi judul dan URL tab yang sedang aktif, lalu kamu tinggal pilih tombol Salin Markdown, Salin HTML, atau Salin URL. Versi lengkapnya juga punya context menu (klik kanan di halaman mana pun, pilih "Salin sebagai Markdown") dan shortcut keyboard Ctrl+Shift+C untuk membuka popup.

Semuanya pakai JavaScript vanilla, tanpa framework dan tanpa build step. Kalau kamu belum kenal DOM, variabel, atau fungsi di JavaScript, saya sudah membahas dasar JavaScript dan DOM di artikel terpisah — baca itu dulu kalau kode di bawah terasa asing.

Struktur Proyek

Sebuah extension Manifest V3 pada dasarnya adalah folder berisi file-file statis. Buat folder baru bernama markdown-link-copier lalu susun seperti ini:

bash
markdown-link-copier/
├── manifest.json
├── popup.html
├── popup.css
├── popup.js
├── background.js
├── offscreen.html
└── offscreen.js

manifest.json: Jantung Extension

manifest.json adalah file yang dibaca Chrome pertama kali. Dia yang menjelaskan nama extension, izin apa saja yang diminta, file mana yang jadi popup, dan file mana yang jadi background service worker. Isi file ini:

json
{
  "manifest_version": 3,
  "name": "Markdown Link Copier",
  "version": "1.0.0",
  "description": "Salin judul dan URL halaman sebagai link Markdown dalam sekali klik.",
  "permissions": ["activeTab", "contextMenus", "clipboardWrite", "offscreen"],
  "action": {
    "default_popup": "popup.html",
    "default_title": "Salin link Markdown"
  },
  "background": {
    "service_worker": "background.js"
  },
  "commands": {
    "_execute_action": {
      "suggested_key": {
        "default": "Ctrl+Shift+C",
        "mac": "Command+Shift+C"
      },
      "description": "Buka popup Markdown Link Copier"
    }
  }
}

Beberapa hal yang perlu kamu pahami dari file ini. manifest_version wajib bernilai 3 — versi 2 sudah tidak diterima Chrome Web Store. Bagian action mengatur ikon toolbar beserta popup-nya. Bagian background menunjuk ke service worker yang akan kita pakai untuk context menu. Terakhir, commands._execute_action memetakan shortcut keyboard ke aksi "buka popup".

Kenapa izinnya activeTab, bukan tabs? Izin tabs memberi akses baca judul dan URL semua tab, dan itu memicu peringatan izin yang menyeramkan saat instalasi. activeTab jauh lebih ramah: izin ini hanya aktif setelah pengguna mengeklik ikon extension atau menu context-nya. Untuk kebutuhan kita membaca tab yang sedang aktif, itu sudah cukup.

Membuat Popup

Popup adalah halaman HTML biasa yang dirender saat kamu klik ikon extension. Buat popup.html dengan tiga tombol salin dan satu area status:

html
<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <h1>Markdown Link</h1>
    <p id="page-title" class="title">Memuat...</p>
    <p id="page-url" class="url"></p>
    <div class="buttons">
      <button id="copy-markdown">Salin Markdown</button>
      <button id="copy-html">Salin HTML</button>
      <button id="copy-url">Salin URL</button>
    </div>
    <p id="status" class="status"></p>
    <script src="popup.js"></script>
  </body>
</html>
css
body {
  font-family: system-ui, -apple-system, sans-serif;
  width: 300px;
  padding: 14px;
  margin: 0;
}

h1 {
  font-size: 15px;
  margin: 0 0 8px;
}

.title {
  font-weight: 600;
  margin: 0 0 2px;
  word-break: break-word;
}

.url {
  color: #6b7280;
  font-size: 12px;
  margin: 0 0 12px;
  word-break: break-all;
}

.buttons {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

button {
  border: 1px solid #d1d5db;
  background: #f9fafb;
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 13px;
  cursor: pointer;
}

button:hover {
  background: #e5e7eb;
}

.status {
  color: #16a34a;
  font-size: 12px;
  margin: 10px 0 0;
  min-height: 16px;
}

Sekarang logikanya di popup.js. Kita baca tab aktif lewat API chrome.tabs, lalu salin teksnya ke clipboard:

javascript
const titleEl = document.getElementById("page-title");
const urlEl = document.getElementById("page-url");
const statusEl = document.getElementById("status");

let currentTitle = "";
let currentUrl = "";

async function loadTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  currentTitle = tab.title || "Tanpa judul";
  currentUrl = tab.url || "";
  titleEl.textContent = currentTitle;
  urlEl.textContent = currentUrl;
}

const markdownLink = () => `[${currentTitle}](${currentUrl})`;
const htmlLink = () => `<a href="${currentUrl}">${currentTitle}</a>`;

async function copy(text, label) {
  await navigator.clipboard.writeText(text);
  statusEl.textContent = `${label} disalin ke clipboard.`;
  setTimeout(() => (statusEl.textContent = ""), 1600);
}

document.getElementById("copy-markdown").addEventListener("click", () => {
  copy(markdownLink(), "Markdown");
});
document.getElementById("copy-html").addEventListener("click", () => {
  copy(htmlLink(), "HTML");
});
document.getElementById("copy-url").addEventListener("click", () => {
  copy(currentUrl, "URL");
});

loadTab();

chrome.tabs.query mengembalikan Promise, jadi kita await hasilnya sebelum membaca properti title dan url. Pola ini sama persis dengan yang saya bahas di panduan async/await di JavaScript. Penulisan ke clipboard lewat navigator.clipboard.writeText berjalan mulus di sini karena popup adalah dokumen dengan fokus, dan klik tombol dihitung sebagai user gesture.

Context Menu dengan Service Worker

Popup saja sudah berguna, tapi saya lebih sering menyalin link lewat klik kanan — tidak perlu menggerakkan kursor sampai ke toolbar. Fitur ini ditangani service worker, yang berjalan di belakang layar tanpa halaman terlihat. Buat background.js:

javascript
// Daftarkan context menu saat extension dipasang atau diperbarui.
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "copy-markdown",
    title: "Salin sebagai Markdown",
    contexts: ["page", "link", "selection"],
  });
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId !== "copy-markdown") return;

  const url = info.linkUrl || (tab && tab.url) || "";
  const title = info.selectionText || (tab && tab.title) || "Link";
  const markdown = `[${title}](${url})`;

  await copyToClipboard(markdown);
});

async function copyToClipboard(text) {
  try {
    await chrome.offscreen.createDocument({
      url: "offscreen.html",
      reasons: [chrome.offscreen.Reason.CLIPBOARD],
      justification: "Menyalin link Markdown dari context menu",
    });
  } catch (err) {
    // Document sudah dibuat sebelumnya; abaikan dan lanjutkan.
  }

  await chrome.runtime.sendMessage({ type: "copy", text });
}

Perhatikan properti contexts. Nilai page menampilkan menu di area kosong halaman, link menampilkannya saat kamu klik kanan sebuah link, dan selection saat ada teks terseleksi. Kalau pengguna menyorot teks lalu klik kanan, teks itu jadi judul link (info.selectionText); kalau tidak, kita pakai judul tab.

Clipboard di Service Worker: Offscreen Document

Di sinilah letak jebakan Manifest V3 yang paling sering bikin bingung. Service worker tidak punya akses ke navigator.clipboard, karena tidak ada dokumen yang bisa menerima fokus. Solusinya: pinjam dokumen tersembunyi bernama offscreen document. Buat dua file ini:

html
<!DOCTYPE html>
<html lang="id">
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <script src="offscreen.js"></script>
  </body>
</html>
javascript
chrome.runtime.onMessage.addListener((message) => {
  if (message.type === "copy") {
    navigator.clipboard.writeText(message.text);
  }
});

Alurnya begini: service worker memanggil chrome.offscreen.createDocument untuk memunculkan offscreen.html (cukup sekali, document itu tetap hidup), lalu mengirim pesan { type: "copy", text }. Listener di offscreen.js menerima pesan itu dan menjalankan navigator.clipboard.writeText di dalam dokumen tersebut. createDocument melempar error bila document sudah ada, makanya kita bungkus dengan try/catch.

Memasang Extension di Chrome

Kita belum memublikasikan apa pun ke Chrome Web Store, jadi pasangnya lewat mode developer. Buka chrome://extensions di Chrome, aktifkan toggle Developer mode di pojok kanan atas, lalu klik Load unpacked dan pilih folder markdown-link-copier. Ikon extension langsung muncul di toolbar.

Uji coba: buka halaman mana pun, klik ikonnya, tekan Salin Markdown, lalu tempel di editor — hasilnya berbentuk [Judul Halaman](https://...). Klik kanan di area kosong halaman dan pilih Salin sebagai Markdown untuk menguji context menu. Kalau ada error, buka halaman extension di chrome://extensions dan klik Errors untuk melihat jejak errornya.

Langkah Berikutnya

Extension ini bisa diperluas ke banyak arah. Tambahkan halaman options supaya pengguna bisa memilih format salin default. Publikasikan ke Chrome Web Store (daftar developer sekali bayar USD 5). Atau kalau kamu tim TypeScript, tulis ulang logikanya dengan TS lalu bundle pakai esbuild atau Vite — Manifest V3 tetap sama, hanya proses build-nya yang bertambah.

Apakah extension ini bisa dipakai di browser lain?

Sebagian besar bisa. Edge dan Brave mendukung Manifest V3 dan API chrome.* hampir identik, jadi folder yang sama bisa langsung di-load. Firefox juga mendukung Manifest V3, tapi API clipboard di service worker-nya berbeda — kamu perlu menyesuaikan bagian offscreen document menjadi pola Firefox (misalnya lewat content script).

Kenapa tidak pakai izin tabs supaya bisa membaca semua tab?

Karena izin tabs memicu peringatan "Read and change all your data on all websites" saat instalasi, yang bikin banyak orang mundur. Izin activeTab cukup untuk membaca judul dan URL tab aktif setelah pengguna mengeklik ikon atau menu context-nya, dan jauh lebih dipercaya pengguna.