Building a Chrome extension to track car rental deals represents a practical intersection of web development skills and real-world utility. This guide walks through the technical implementation of creating an extension that monitors rental prices, alerts users to deals, and provides price comparison functionality.
Understanding the Architecture
A car rental deal tracker extension operates through three core components: a content script that scrapes pricing data from rental company websites, a background service worker that manages price alerts and storage, and a popup interface for users to view tracked deals. Modern Chrome extensions use Manifest V3, which requires adjustments to how background scripts operate compared to older V2 implementations.
The fundamental challenge lies in the dynamic nature of car rental pricing. Prices fluctuate based on location, dates, vehicle type, and demand. Your extension needs to handle these variables while remaining efficient and respecting website terms of service.
Project Structure
Create the following directory structure for your extension:
car-rental-deals/
├── manifest.json
├── popup/
│ ├── popup.html
│ └── popup.js
├── content/
│ └── content.js
├── background/
│ └── background.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
Manifest Configuration
Your manifest.json defines the extension’s capabilities and permissions:
{
"manifest_version": 3,
"name": "Car Rental Deal Tracker",
"version": "1.0",
"description": "Track and compare car rental prices across providers",
"permissions": [
"storage",
"notifications",
"activeTab",
"scripting"
],
"host_permissions": [
"https://*.hertz.com/*",
"https://*.avis.com/*",
"https://*.enterprise.com/*",
"https://*.budget.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/background.js"
},
"content_scripts": [
{
"matches": [
"https://www.hertz.com/*",
"https://www.avis.com/*"
],
"js": ["content/content.js"]
}
]
}
Content Script Implementation
The content script runs on rental company pages and extracts pricing information. Different websites structure their data differently, so you need adaptive selectors:
// content/content.js
// Pricing selectors for different rental companies
const SELECTORS = {
'hertz.com': {
vehicle: '.vehicle-card .vehicle-title',
price: '.vehicle-card .price-amount',
perDay: '.vehicle-card .per-day-rate'
},
'avis.com': {
vehicle: '.vehicle-result__name',
price: '.vehicle-result__price',
perDay: '.vehicle-result__daily'
},
'enterprise.com': {
vehicle: '[data-vehicle-name]',
price: '[data-total-price]',
perDay: '[data-daily-rate]'
}
};
function detectProvider() {
const hostname = window.location.hostname;
for (const domain of Object.keys(SELECTORS)) {
if (hostname.includes(domain.replace('www.', ''))) {
return domain;
}
}
return null;
}
function extractPricingData() {
const provider = detectProvider();
if (!provider || !SELECTORS[provider]) {
return null;
}
const vehicles = document.querySelectorAll('.vehicle-card, .vehicle-result__container, [data-vehicle-id]');
const deals = [];
vehicles.forEach(vehicle => {
const nameEl = vehicle.querySelector(SELECTORS[provider].vehicle);
const priceEl = vehicle.querySelector(SELECTORS[provider].price);
const perDayEl = vehicle.querySelector(SELECTORS[provider].perDay);
if (nameEl && priceEl) {
const priceText = priceEl.textContent.replace(/[^0-9.]/g, '');
deals.push({
provider: provider,
vehicle: nameEl.textContent.trim(),
totalPrice: parseFloat(priceText) || 0,
perDay: perDayEl ? parseFloat(perDayEl.textContent.replace(/[^0-9.]/g, '')) : 0,
url: window.location.href,
timestamp: Date.now()
});
}
});
return deals;
}
// Listen for messages from popup or background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getPricing') {
const deals = extractPricingData();
sendResponse({ deals: deals });
}
return true;
});
Background Service Worker
The background script manages data storage and price alerts. With Manifest V3, use persistent storage and handle alerts through the notifications API:
// background/background.js
const DB_NAME = 'CarRentalDealsDB';
const DB_VERSION = 1;
let db;
// Initialize IndexedDB for persistent storage
function initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
db = request.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const database = event.target.result;
if (!database.objectStoreNames.contains('deals')) {
const store = database.createObjectStore('deals', { keyPath: 'id', autoIncrement: true });
store.createIndex('provider', 'provider', { unique: false });
store.createIndex('vehicle', 'vehicle', { unique: false });
}
};
});
}
// Save deals to IndexedDB
async function saveDeals(deals) {
if (!db) await initDB();
const transaction = db.transaction(['deals'], 'readwrite');
const store = transaction.objectStore('deals');
for (const deal of deals) {
store.add(deal);
}
return new Promise((resolve, reject) => {
transaction.oncomplete = resolve;
transaction.onerror = () => reject(transaction.error);
});
}
// Check for price drops and notify user
async function checkPriceAlerts(newDeals, userId) {
const oldDeals = await getStoredDeals(userId);
for (const newDeal of newDeals) {
const matchingOld = oldDeals.find(
d => d.vehicle === newDeal.vehicle && d.provider === newDeal.provider
);
if (matchingOld && newDeal.totalPrice < matchingOld.totalPrice * 0.9) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'Price Drop Alert!',
message: `${newDeal.vehicle} dropped to $${newDeal.totalPrice} at ${newDeal.provider}`
});
}
}
}
function getStoredDeals(userId) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['deals'], 'readonly');
const store = transaction.objectStore('deals');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Handle messages from content scripts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'saveDeals') {
saveDeals(request.deals).then(() => sendResponse({ success: true }));
return true;
}
if (request.action === 'getStoredDeals') {
getStoredDeals(request.userId).then(deals => sendResponse({ deals }));
return true;
}
});
Popup Interface
The popup provides users with a view of their tracked deals and current prices:
<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body { width: 320px; padding: 16px; font-family: -apple-system, system-ui, sans-serif; }
h2 { margin: 0 0 12px; font-size: 16px; }
.deal { padding: 8px; border-bottom: 1px solid #eee; }
.deal:last-child { border-bottom: none; }
.provider { font-size: 11px; color: #666; text-transform: uppercase; }
.vehicle { font-weight: 600; margin: 4px 0; }
.price { color: #2ecc71; font-weight: 600; }
.refresh-btn { width: 100%; padding: 8px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
.refresh-btn:hover { background: #0056b3; }
</style>
</head>
<body>
<h2>Car Rental Deals</h2>
<div id="deals-container"></div>
<button id="refresh-btn" class="refresh-btn">Refresh Prices</button>
<script src="popup.js"></script>
</body>
</html>
// popup/popup.js
document.addEventListener('DOMContentLoaded', () => {
loadDeals();
document.getElementById('refresh-btn').addEventListener('click', () => {
refreshPrices();
});
});
async function loadDeals() {
const response = await chrome.runtime.sendMessage({ action: 'getStoredDeals', userId: 'default' });
const deals = response.deals || [];
renderDeals(deals);
}
function renderDeals(deals) {
const container = document.getElementById('deals-container');
if (deals.length === 0) {
container.innerHTML = '<p style="color: #666; text-align: center;">No deals tracked yet. Visit a rental site to start tracking.</p>';
return;
}
// Sort by price
deals.sort((a, b) => a.totalPrice - b.totalPrice);
container.innerHTML = deals.slice(0, 10).map(deal => `
<div class="deal">
<div class="provider">${deal.provider}</div>
<div class="vehicle">${deal.vehicle}</div>
<div class="price">$${deal.totalPrice.toFixed(2)} ($${deal.perDay.toFixed(2)}/day)</div>
</div>
`).join('');
}
async function refreshPrices() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, { action: 'getPricing' }, async (response) => {
if (response && response.deals) {
await chrome.runtime.sendMessage({
action: 'saveDeals',
deals: response.deals
});
loadDeals();
}
});
}
Key Implementation Considerations
When building price tracking extensions, consider rate limiting your requests to avoid triggering anti-bot measures on rental websites. Implement exponential backoff when requests fail, and always respect robots.txt files and website terms of service.
Data storage presents another challenge. Chrome’s chrome.storage API offers convenient sync across devices but has storage limits. For larger datasets, IndexedDB provides more capacity but requires more complex handling.
Price alert functionality requires careful threshold tuning. A 10% price drop threshold typically captures meaningful deals while avoiding notification fatigue from minor fluctuations.
Testing Your Extension
Load your extension in Chrome by navigating to chrome://extensions/, enabling Developer mode, and clicking “Load unpacked”. Test on multiple rental websites to verify selector accuracy. Use Chrome DevTools to debug content scripts and inspect network requests for pricing data.
Consider adding logging throughout your code:
console.log('[CarRentalDeals] Extension loaded');
console.log('[CarRentalDeals] Detected provider:', provider);
console.log('[CarRentalDeals] Found deals:', deals.length);
Building a functional car rental deal tracker demonstrates practical skills in Chrome extension development, web scraping techniques, IndexedDB usage, and notification systems—all valuable competencies for developer portfolios and real-world applications.
Related Reading
- Claude Code for Beginners: Complete Getting Started Guide
- Best Claude Skills for Developers in 2026
- Claude Skills Guides Hub
Built by theluckystrike — More at zovo.one