Build a GitHub Enhancer Chrome Extension: Complete Developer Guide

28 min read

Build a GitHub Enhancer Chrome Extension: Complete Developer Guide

Build a GitHub Enhancer Chrome Extension: Complete Developer Guide

GitHub is the backbone of modern software development, hosting millions of repositories and serving as the collaboration hub for developers worldwide. While GitHub’s native features are powerful, there’s always room for improvement in terms of workflow efficiency and user experience. This is where a custom GitHub enhancer extension can transform your development routine.

In this comprehensive guide, we’ll walk you through building a fully functional GitHub enhancer Chrome extension that adds valuable features to GitHub’s web interface. Whether you want to automate code reviews, enhance repository navigation, or add productivity shortcuts, this tutorial will give you the foundation to create powerful GitHub tools Chrome extensions that thousands of developers will want to use.


Why Build a GitHub Enhancer Extension?

The demand for GitHub productivity extensions has never been higher. Developers spend countless hours on GitHub daily, and even small improvements in workflow can translate to hours of saved time over weeks and months. Building a github enhancer extension offers several compelling reasons to start this project:

Solving Real Problems

Popular extensions like GitHub Desktop, Octotree, and Refined GitHub have millions of users because they address genuine pain points in the developer experience. A well-designed GitHub enhancer extension can solve problems like difficulty navigating large codebases, repetitive review tasks, or lack of customization options. When you build something that solves your own problems, you have a ready-made user base.

Learning Modern Extension Development

This project serves as an excellent learning opportunity for Chrome extension development. You’ll work with essential APIs including the Chrome Storage API for persisting user preferences, the Chrome Messaging API for communication between components, and content scripts for DOM manipulation. These skills transfer directly to building other extensions for different websites.

Building Your Portfolio

A GitHub enhancer extension demonstrates proficiency in JavaScript, browser APIs, and user interface design. When potential employers or clients see a functional extension with real users, it speaks volumes about your ability to build practical, user-centered software.

Monetization Potential

The developer tools market continues to grow, and GitHub-related extensions can be monetized through freemium models, premium features, or donations. With Chrome’s billions of users and GitHub’s massive developer community, the potential audience for a quality GitHub tools Chrome extension is substantial.


Project Planning and Features

Before writing any code, let’s define what our GitHub enhancer extension will do. A comprehensive github productivity extension typically includes several categories of features:

Core Features We’ll Build

  1. Quick Repository Navigation: Fast keyboard shortcuts for jumping between repositories, branches, and pull requests
  2. Code Review Enhancements: Visual indicators for code changes, inline commenting shortcuts, and review status tracking
  3. Repository Insights: Quick access to commit history, contributor statistics, and open issue counts
  4. Customizable UI Tweaks: Theme adjustments, font preferences, and layout modifications
  5. Notification Management: Quick filters and batch actions for GitHub notifications

Technical Requirements

Our extension will use Manifest V3, the latest Chrome extension platform, ensuring compatibility with modern Chrome security policies and performance standards. We’ll implement:

  • Background service workers for handling GitHub API requests
  • Content scripts for DOM manipulation and UI injection
  • Popup interface for quick actions and settings
  • Chrome Storage for persisting user preferences
  • Message passing for component communication

Setting Up the Development Environment

Every Chrome extension begins with a well-organized project structure. Let’s set up our development environment properly.

Project Directory Structure

Create a new folder for your extension and set up the following structure:

github-enhancer/
├── manifest.json
├── background.js
├── popup/
│   ├── popup.html
│   ├── popup.css
│   └── popup.js
├── content/
│   └── content.js
├── options/
│   ├── options.html
│   └── options.js
├── styles/
│   └── injected.css
├── icons/
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
└── utils/
    └── helpers.js

This structure keeps different components organized and makes maintenance easier as your extension grows.

Creating the Manifest File

The manifest.json file is the heart of every Chrome extension. For our GitHub enhancer, we’ll use Manifest V3 with the permissions we need:

{
  "manifest_version": 3,
  "name": "GitHub Enhancer",
  "version": "1.0.0",
  "description": "Supercharge your GitHub workflow with productivity tools and customization options",
  "permissions": [
    "storage",
    "activeTab",
    "scripting",
    "notifications"
  ],
  "host_permissions": [
    "https://github.com/*",
    "https://api.github.com/*"
  ],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["https://github.com/*"],
      "js": ["content/content.js"],
      "css": ["styles/injected.css"],
      "run_at": "document_idle"
    }
  ],
  "options_page": "options/options.html",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

This manifest declares all necessary permissions while following security best practices. We request access only to GitHub domains, and we use content scripts that run on specific pages rather than broad permissions.


Building the Content Script

Content scripts are the bridge between your extension and the web pages users visit. For a GitHub enhancer extension, content scripts will inject functionality directly into GitHub’s interface.

Detecting GitHub Pages

First, we need to ensure our script only runs on relevant GitHub pages:

// content/content.js

(function() {
  'use strict';

  // Check if we're on a GitHub repository page
  function isRepositoryPage() {
    return window.location.pathname.match(/^\/[^/]+\/[^/]+$/);
  }

  // Check if we're on a pull request page
  function isPullRequestPage() {
    return window.location.pathname.match(/^\/[^/]+\/[^/]+\/pull\/\d+$/);
  }

  // Check if we're on an issues page
  function isIssuesPage() {
    return window.location.pathname.match(/^\/[^/]+\/[^/]+\/issues$/);
  }

  // Initialize based on current page
  function init() {
    if (isRepositoryPage()) {
      initRepositoryFeatures();
    } else if (isPullRequestPage()) {
      initPullRequestFeatures();
    } else if (isIssuesPage()) {
      initIssuesFeatures();
    }
  }

  // Repository page enhancements
  function initRepositoryFeatures() {
    console.log('GitHub Enhancer: Initializing repository features');
    // Add your repository enhancements here
  }

  // Pull request page enhancements
  function initPullRequestFeatures() {
    console.log('GitHub Enhancer: Initializing PR features');
    // Add your PR enhancements here
  }

  // Issues page enhancements
  function initIssuesFeatures() {
    console.log('GitHub Enhancer: Initializing issues features');
    // Add your issues enhancements here
  }

  // Wait for page to be fully loaded
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

This foundation gives us a modular approach where we can add features specific to each GitHub page type.

Adding Repository Navigation Features

Let’s add practical features that enhance repository navigation:

// Inside initRepositoryFeatures()
function initRepositoryFeatures() {
  addQuickBranchSwitcher();
  addCommitCountIndicator();
  enhanceFileExplorer();
}

// Quick branch switcher
function addQuickBranchSwitcher() {
  // Find the branch selector button
  const branchButton = document.querySelector('.branch-select-button');
  if (!branchButton) return;

  // Create keyboard shortcut hint
  const hint = document.createElement('span');
  hint.className = 'github-enhancer-hint';
  hint.textContent = 'Press B to switch branch';
  hint.style.cssText = 'margin-left: 8px; font-size: 12px; color: #7d8590;';

  branchButton.parentNode.appendChild(hint);

  // Listen for keyboard shortcut
  document.addEventListener('keydown', (e) => {
    if (e.key === 'b' && !e.ctrlKey && !e.metaKey && 
        document.activeElement.tagName !== 'INPUT' &&
        document.activeElement.tagName !== 'TEXTAREA') {
      branchButton.click();
    }
  });
}

// Add commit count to repository header
function addCommitCountIndicator() {
  const commitLink = document.querySelector('[data-tab-item="commits"]');
  if (!commitLink) return;

  // Fetch commit count using GitHub API
  fetch('/' + getRepositoryPath() + '/commits?per_page=1', {
    headers: {
      'Accept': 'application/vnd.github.v3+json'
    }
  }).then(response => {
    const link = response.headers.get('Link');
    if (link) {
      const match = link.match(/page=(\d+)>; rel="last"/);
      if (match) {
        const count = parseInt(match[1]);
        addCommitBadge(commitLink, count);
      }
    }
  }).catch(console.error);
}

function addCommitBadge(element, count) {
  const badge = document.createElement('span');
  badge.className = 'github-enhancer-badge';
  badge.textContent = formatNumber(count);
  badge.style.cssText = `
    background: #238636;
    color: white;
    padding: 2px 8px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 600;
    margin-left: 8px;
  `;
  element.appendChild(badge);
}

function getRepositoryPath() {
  const path = window.location.pathname;
  const match = path.match(/^\/([^/]+)\/([^/]+)/);
  return match ? `${match[1]}/${match[2]}` : '';
}

function formatNumber(num) {
  if (num >= 1000000) {
    return (num / 1000000).toFixed(1) + 'M';
  } else if (num >= 1000) {
    return (num / 1000).toFixed(1) + 'K';
  }
  return num.toString();
}

Enhancing Pull Request Reviews

For pull request pages, we can add features that make code review more efficient:

function initPullRequestFeatures() {
  addPRSizeIndicator();
  addReviewShortcutHints();
  highlightChangedFiles();
}

// Show PR size impact
function addPRSizeIndicator() {
  const prHeader = document.querySelector('.gh-header-sticky');
  if (!prHeader) return;

  // Calculate total changes
  const additions = document.querySelector('.addition');
  const deletions = document.querySelector('.deletion');

  if (additions || deletions) {
    const total = (additions?.textContent || '0').replace(/,/g, '') * 1 +
                  (deletions?.textContent || '0').replace(/,/g, '') * 1;

    const indicator = document.createElement('div');
    indicator.className = 'pr-size-indicator';
    indicator.textContent = total > 400 ? '⚠️ Large PR' : '✓ Manageable';
    indicator.style.cssText = `
      padding: 4px 12px;
      border-radius: 6px;
      font-size: 12px;
      font-weight: 600;
      background: ${total > 400 ? '#da3633' : '#238636'};
      color: white;
    `;

    prHeader.querySelector('.flex-1')?.appendChild(indicator);
  }
}

// Review keyboard shortcuts
function addReviewShortcutHints() {
  const reviewDropdown = document.querySelector('.review-menu-trigger');
  if (!reviewDropdown) return;

  const shortcuts = document.createElement('div');
  shortcuts.className = 'review-shortcuts';
  shortcuts.innerHTML = `
    <div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #30363d; font-size: 11px; color: #7d8590;">
      <div>Submit: Ctrl+Enter</div>
      <div>Close: Ctrl+Shift+C</div>
      <div>Files: F</div>
    </div>
  `;
  reviewDropdown.closest('.dropdown')?.querySelector('.dropdown-menu')?.appendChild(shortcuts);
}

Building the Background Service Worker

The background service worker handles tasks that run independently of any particular web page, making it perfect for API calls, notifications, and cross-tab communication.

// background.js

// GitHub API configuration
const GITHUB_API_BASE = 'https://api.github.com';

// Handle messages from content scripts and popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.type) {
    case 'GET_REPOSITORY_INFO':
      handleGetRepositoryInfo(message.data, sendResponse);
      return true;

    case 'GET_USER_PREFERENCES':
      handleGetUserPreferences(sendResponse);
      return true;

    case 'SET_USER_PREFERENCES':
      handleSetUserPreferences(message.data, sendResponse);
      return true;

    case 'FETCH_NOTIFICATIONS':
      handleFetchNotifications(sendResponse);
      return true;

    default:
      console.warn('Unknown message type:', message.type);
  }
});

// Get repository information from GitHub API
async function handleGetRepositoryInfo(data, sendResponse) {
  const { owner, repo } = data;

  try {
    const response = await fetch(
      `${GITHUB_API_BASE}/repos/${owner}/${repo}`,
      {
        headers: {
          'Accept': 'application/vnd.github.v3+json',
          'Authorization': `Bearer ${await getToken()}`
        }
      }
    );

    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status}`);
    }

    const repoInfo = await response.json();
    sendResponse({ success: true, data: repoInfo });
  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

// Get user preferences from storage
async function handleGetUserPreferences(sendResponse) {
  try {
    const result = await chrome.storage.local.get(['preferences']);
    sendResponse({ success: true, data: result.preferences || {} });
  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

// Save user preferences
async function handleSetUserPreferences(data, sendResponse) {
  try {
    await chrome.storage.local.set({ preferences: data });
    sendResponse({ success: true });
  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

// Fetch GitHub notifications
async function handleFetchNotifications(sendResponse) {
  try {
    const response = await fetch(
      `${GITHUB_API_BASE}/notifications`,
      {
        headers: {
          'Accept': 'application/vnd.github.v3+json',
          'Authorization': `Bearer ${await getToken()}`
        }
      }
    );

    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status}`);
    }

    const notifications = await response.json();
    sendResponse({ success: true, data: notifications });
  } catch (error) {
    sendResponse({ success: false, error: error.message });
  }
}

// Helper to get stored GitHub token
async function getToken() {
  const result = await chrome.storage.local.get(['github_token']);
  return result.github_token || '';
}

// Handle extension installation
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    // Set default preferences
    chrome.storage.local.set({
      preferences: {
        enableShortcuts: true,
        showCommitCount: true,
        enableNotifications: true,
        theme: 'system'
      }
    });
    console.log('GitHub Enhancer: Extension installed');
  }
});

The popup provides quick access to extension features without leaving the current tab.

<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>GitHub Enhancer</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="popup-container">
    <header class="popup-header">
      <h1>GitHub Enhancer</h1>
      <span class="version">v1.0.0</span>
    </header>

    <section class="quick-actions">
      <button id="openRepoBtn" class="action-btn">
        <span class="icon">📂</span>
        Open Repository
      </button>
      <button id="newIssueBtn" class="action-btn">
        <span class="icon">🐙</span>
        New Issue
      </button>
    </section>

    <section class="notifications-section">
      <h2>Notifications</h2>
      <div id="notificationsList" class="notifications-list">
        <div class="loading">Loading...</div>
      </div>
    </section>

    <section class="stats-section">
      <h2>Quick Stats</h2>
      <div id="repoStats" class="stats-grid">
        <div class="stat">
          <span class="stat-value" id="starCount">-</span>
          <span class="stat-label">Stars</span>
        </div>
        <div class="stat">
          <span class="stat-value" id="forkCount">-</span>
          <span class="stat-label">Forks</span>
        </div>
        <div class="stat">
          <span class="stat-value" id="issueCount">-</span>
          <span class="stat-label">Issues</span>
        </div>
        <div class="stat">
          <span class="stat-value" id="prCount">-</span>
          <span class="stat-label">PRs</span>
        </div>
      </div>
    </section>

    <footer class="popup-footer">
      <a href="#" id="optionsLink">Settings</a>
      <a href="#" id="githubLink">GitHub</a>
    </footer>
  </div>
  <script src="popup.js"></script>
</body>
</html>

Add some styling to make it visually appealing:

/* popup/popup.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 360px;
  min-height: 400px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  background: #0d1117;
  color: #c9d1d9;
}

.popup-container {
  padding: 16px;
}

.popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid #30363d;
}

.popup-header h1 {
  font-size: 18px;
  font-weight: 600;
  color: #f0f6fc;
}

.version {
  font-size: 12px;
  color: #7d8590;
}

.quick-actions {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}

.action-btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 12px;
  background: #21262d;
  border: 1px solid #30363d;
  border-radius: 6px;
  color: #c9d1d9;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
}

.action-btn:hover {
  background: #30363d;
  border-color: #8b949e;
}

.icon {
  font-size: 16px;
}

.notifications-section,
.stats-section {
  margin-bottom: 16px;
}

.notifications-section h2,
.stats-section h2 {
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 12px;
  color: #f0f6fc;
}

.notifications-list {
  max-height: 150px;
  overflow-y: auto;
}

.notification-item {
  padding: 8px;
  background: #161b22;
  border-radius: 6px;
  margin-bottom: 6px;
  font-size: 12px;
}

.notification-item a {
  color: #58a6ff;
  text-decoration: none;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}

.stat {
  background: #161b22;
  border-radius: 6px;
  padding: 12px;
  text-align: center;
}

.stat-value {
  display: block;
  font-size: 20px;
  font-weight: 700;
  color: #f0f6fc;
}

.stat-label {
  font-size: 11px;
  color: #7d8590;
  text-transform: uppercase;
}

.popup-footer {
  display: flex;
  justify-content: space-between;
  padding-top: 12px;
  border-top: 1px solid #30363d;
}

.popup-footer a {
  font-size: 12px;
  color: #58a6ff;
  text-decoration: none;
}

.popup-footer a:hover {
  text-decoration: underline;
}

.loading {
  text-align: center;
  color: #7d8590;
  padding: 20px;
}
// popup/popup.js
document.addEventListener('DOMContentLoaded', () => {
  loadCurrentRepository();
  loadNotifications();
  setupEventListeners();
});

async function loadCurrentRepository() {
  // Get current tab's URL
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const url = new URL(tab.url);

  // Extract owner and repo from GitHub URL
  const pathParts = url.pathname.split('/').filter(Boolean);
  if (pathParts.length >= 2 && url.hostname.includes('github.com')) {
    const [owner, repo] = pathParts.slice(0, 2);
    loadRepositoryStats(owner, repo);
  }
}

async function loadRepositoryStats(owner, repo) {
  try {
    const response = await chrome.runtime.sendMessage({
      type: 'GET_REPOSITORY_INFO',
      data: { owner, repo }
    });

    if (response.success) {
      const data = response.data;
      document.getElementById('starCount').textContent = formatNumber(data.stargazers_count);
      document.getElementById('forkCount').textContent = formatNumber(data.forks_count);
      document.getElementById('issueCount').textContent = formatNumber(data.open_issues_count);
      // For PRs, we'd need a separate API call
      document.getElementById('prCount').textContent = '-';
    }
  } catch (error) {
    console.error('Failed to load repo stats:', error);
  }
}

async function loadNotifications() {
  const list = document.getElementById('notificationsList');

  try {
    const response = await chrome.runtime.sendMessage({
      type: 'FETCH_NOTIFICATIONS'
    });

    if (response.success && response.data.length > 0) {
      list.innerHTML = response.data.slice(0, 5).map(notification => `
        <div class="notification-item">
          <a href="${notification.subject.url}" target="_blank">
            ${notification.subject.title}
          </a>
        </div>
      `).join('');
    } else {
      list.innerHTML = '<div class="notification-item">No new notifications</div>';
    }
  } catch (error) {
    list.innerHTML = '<div class="notification-item">Sign in to see notifications</div>';
  }
}

function setupEventListeners() {
  document.getElementById('openRepoBtn').addEventListener('click', () => {
    chrome.tabs.create({ url: 'https://github.com' });
  });

  document.getElementById('newIssueBtn').addEventListener('click', async () => {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    const url = new URL(tab.url);
    const pathParts = url.pathname.split('/').filter(Boolean);

    if (pathParts.length >= 2) {
      const [owner, repo] = pathParts.slice(0, 2);
      chrome.tabs.create({ url: `https://github.com/${owner}/${repo}/issues/new` });
    }
  });

  document.getElementById('optionsLink').addEventListener('click', (e) => {
    e.preventDefault();
    chrome.runtime.openOptionsPage();
  });

  document.getElementById('githubLink').addEventListener('click', (e) => {
    e.preventDefault();
    chrome.tabs.create({ url: 'https://github.com/theluckystrike/chrome-extension-guide' });
  });
}

function formatNumber(num) {
  if (num >= 1000000) {
    return (num / 1000000).toFixed(1) + 'M';
  } else if (num >= 1000) {
    return (num / 1000).toFixed(1) + 'K';
  }
  return num.toString();
}

Testing and Debugging Your Extension

Before publishing, thorough testing ensures a quality user experience.

Loading Your Extension

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable “Developer mode” in the top right corner
  3. Click “Load unpacked” and select your extension’s folder
  4. Test on various GitHub pages

Common Issues and Solutions

Content script not loading: Check that your matches pattern in manifest.json correctly targets GitHub URLs. Use chrome://extensions/ and check for any error messages.

API requests failing: Ensure you’re using the correct GitHub API endpoints and headers. For higher rate limits, you’ll need to authenticate with a GitHub token.

Popup not responding: Check the background service worker console for errors. Ensure message passing is working correctly.

Debugging Tips

Use console.log statements freely in your content scripts and background worker. Access the background worker console through chrome://extensions/ by clicking “service worker” link under your extension.


Publishing Your Extension

Once your extension is ready, follow these steps to publish to the Chrome Web Store:

  1. Prepare your build: Remove all debug code and ensure production settings
  2. Create screenshots: Take high-quality screenshots of your extension in action
  3. Write a compelling description: Include keywords naturally, such as “github enhancer extension” and “github productivity extension”
  4. Upload through Developer Dashboard: Visit Chrome Web Store Developer Dashboard and create a new listing
  5. Submit for review: Google reviews new extensions, typically within 24-72 hours

Conclusion

Building a GitHub enhancer Chrome extension is an excellent project that combines practical problem-solving with modern web development skills. In this guide, we’ve covered the essential components: Manifest V3 configuration, content scripts for DOM manipulation, background service workers for API handling, and popup interfaces for quick actions.

The foundation we’ve built can be extended with many more features: automated code quality checks, GitHub Actions integration, custom themes, and more. The key is to start with features that solve your own problems, then iterate based on user feedback.

Remember that successful GitHub tools Chrome extensions focus on specific pain points rather than trying to do everything. As you develop your extension, pay attention to what features users engage with most and continue refining those experiences.

With this guide, you have everything you need to start building your own github enhancer extension. The developer community is always excited about tools that improve their workflow, and your extension could be the next must-have in every developer’s browser.

No previous article
No next article