Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -983,8 +983,8 @@
{
"name": "modern-web-guidance",
"source": {
"source": "url",
"url": "https://github.com/GoogleChrome/modern-web-guidance.git"
"source": "local",
"path": "./plugins/modern-web-guidance"
},
"policy": {
"installation": "AVAILABLE",
Expand Down
5 changes: 1 addition & 4 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -1369,10 +1369,7 @@
"category": "development",
"keywords": ["web", "best-practices", "chrome", "baseline", "css", "performance"],
"tags": ["skill", "web"],
"source": {
"source": "github",
"repo": "GoogleChrome/modern-web-guidance"
},
"source": "./plugins/modern-web-guidance",
"homepage": "https://github.com/GoogleChrome/modern-web-guidance"
},
{
Expand Down
5 changes: 5 additions & 0 deletions .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@
"source": "./plugins/genkit",
"description": "Official Genkit skills - build AI-powered applications with Genkit in JavaScript/TypeScript, Go, Dart/Flutter, and Python: flows, generation, prompts, tool calling, agents, and model providers"
},
{
"name": "modern-web-guidance",
"source": "./plugins/modern-web-guidance",
"description": "Keep your coding agent up to date with the latest web best practices from the Google Chrome team"
},
{
"name": "java-development",
"source": "./plugins/java-development",
Expand Down
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"plugins/vue": "1.3.3",
"plugins/vueuse": "1.3.2",
"plugins/web-design": "1.4.3",
"plugins/modern-web-guidance": "0.0.196",
"plugins/mastra": "1.7.4",
"plugins/nuxt-ui": "1.6.5",
"plugins/firebase": "1.1.3",
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,12 @@ End-to-end typesafe APIs with oRPC v2 — the core `os` builder, routers, middle

**Install:** `/plugin install orpc@pleaseai` | **Source:** [plugins/orpc](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/orpc)

#### Modern Web Guidance

Keep your coding agent up to date with the latest web best practices from the Google Chrome team — modern HTML/CSS/JS patterns (`modern-web-guidance`) and Manifest V3 Chrome extensions (`chrome-extensions`). Vendored from [GoogleChrome/modern-web-guidance](https://github.com/GoogleChrome/modern-web-guidance) (Apache-2.0) and refreshed weekly by `update-skills.yml`.

**Install:** `/plugin install modern-web-guidance@pleaseai` | **Source:** [plugins/modern-web-guidance](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/modern-web-guidance)

#### evlog

Wide-event logging for TypeScript with [evlog](https://www.evlog.dev): review logging patterns and guide evlog adoption (`review-logging-patterns`), design tamper-aware audit trails (`build-audit-logs`), and debug from `.evlog/logs/` output (`analyze-logs`). Bundles the read-only evlog docs MCP server (`https://www.evlog.dev/mcp`) for current docs pages and log-cost estimates.
Expand Down
530 changes: 530 additions & 0 deletions plugins/modern-web-guidance/.agents/skills/chrome-extensions/SKILL.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Calling External APIs from Extensions

## Permissions

Ordinarily, fetch requests made by extensions follow normal CORS rules.

To determine if this is sufficient, use `curl` to call the API with a test origin. For example:

```
curl -H "Origin: https://example.com" -I https://api.openweathermap.org/data/2.5/weather?q=London&appid=KEY`
```

If the response includes either `*` or `https://example.com` as the value for the `Access-Control-Allow-Origin` header, the API supports CORS.

If the API does not support CORS, request host permissions to bypass these restrictions:

```json
{
"host_permissions": [
"https://no-cors-api.example.com/*"
]
}
```

**Do NOT use `<all_urls>` just for API calls.** Scope to the specific API domains.

## Where to Make API Calls

API calls work from any extension context (service worker, popup, side panel, content scripts):

```js
// From popup or service worker
const response = await fetch('https://api.openweathermap.org/data/2.5/weather?q=London&appid=KEY');
const data = await response.json();
```

**Content scripts** can also make fetch calls, but they follow the web page's CORS rules.

## Error Handling Pattern

```js
async function callAPI(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (err) {
if (err instanceof TypeError) {
// Network error (offline, DNS failure, etc.)
console.error('Network error:', err.message);
} else {
console.error('API error:', err.message);
}
return null;
}
}
```

## API Keys

- Never hardcode API keys in published extensions
- Use `chrome.storage.local` for user-provided keys
- For your own backend, use `chrome.identity` to authenticate instead of embedding keys
- Mark placeholder keys clearly: `const API_KEY = 'YOUR_API_KEY_HERE';`

## Service Worker Considerations

If making API calls from the service worker, remember it can terminate. For long-polling or
webhook-style patterns, use `chrome.offscreen` to create an offscreen document that stays alive,
or use `chrome.alarms` for periodic polling.
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Authentication with chrome.identity

## Setup

```json
{
"permissions": ["identity"],
"oauth2": {
"client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
"scopes": [
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email"
]
}
}
```

## Getting an OAuth Token

```js
async function signIn() {
return new Promise((resolve, reject) => {
chrome.identity.getAuthToken({ interactive: true }, (token) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(token);
}
});
});
}
```

Or with the promise-based API (Chrome 116+):
```js
const { token } = await chrome.identity.getAuthToken({ interactive: true });
```

## Fetching User Profile

```js
async function getUserProfile(token) {
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) throw new Error('Failed to fetch profile');
return response.json();
// Returns: { sub, name, given_name, family_name, picture, email, email_verified }
}
```

## Sign Out

```js
async function signOut(token) {
// Remove cached token
await chrome.identity.removeCachedAuthToken({ token });

// Optionally revoke the token server-side
await fetch(`https://accounts.google.com/o/oauth2/revoke?token=${token}`);
}
```

## Error Handling

```js
try {
const { token } = await chrome.identity.getAuthToken({ interactive: true });
const profile = await getUserProfile(token);
displayProfile(profile);
} catch (err) {
if (err.message.includes('canceled')) {
showMessage('Sign-in was cancelled');
} else if (err.message.includes('not granted')) {
showMessage('Permission was denied');
} else {
showMessage('Sign-in failed: ' + err.message);
}
}
```

## Setting Up Google Cloud Console

1. Go to console.cloud.google.com
2. Create a project (or select existing)
3. Enable "Google People API" or "Google OAuth2 API"
4. Create OAuth 2.0 credentials → Chrome Extension type
5. Set the Application ID to your extension's ID
6. Copy the client_id to your manifest.json

### Extension ID: Development vs Production

**This is critical and often missed.** The OAuth `client_id` is tied to a specific extension ID.
The extension ID changes depending on how you load the extension:

| Context | How ID is determined |
|---------|---------------------|
| Unpacked (development) | Derived from the extension's directory path — changes if you move the folder |
| Packed (.crx) | Derived from the private key used to pack |
| Chrome Web Store | Assigned by the store, permanent |

**To get a stable ID during development**, add a `"key"` field to your manifest.json.
This ensures the same extension ID regardless of directory path:

1. Pack your extension once (`chrome://extensions` → Pack Extension)
2. Open the generated `.crx` as a ZIP, extract the `key` from its manifest
3. Add that key to your development manifest:

```json
{
"key": "MIIBIjANBgkqhk...your-public-key-here...",
"manifest_version": 3,
"name": "My Extension"
}
```

Alternatively, note your unpacked extension's ID from `chrome://extensions` and configure
the OAuth client for that specific ID. Just be aware it will change if the folder moves.

**Always tell users:** "After publishing to the Chrome Web Store, update your OAuth client
configuration with the store-assigned extension ID."

## Non-Google OAuth (launchWebAuthFlow)

For third-party OAuth providers (GitHub, Twitter, etc.):

```js
const redirectUrl = chrome.identity.getRedirectURL();
// Returns: https://<extension-id>.chromiumapp.org/

const authUrl = `https://github.com/login/oauth/authorize?client_id=XXX&redirect_uri=${redirectUrl}`;

const responseUrl = await chrome.identity.launchWebAuthFlow({
url: authUrl,
interactive: true
});

// Parse the token from responseUrl
const url = new URL(responseUrl);
const code = url.searchParams.get('code');
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Content Scripts & DOM Manipulation

## Two Ways to Inject

### 1. Static (manifest declaration)
```json
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content/content.js"],
"css": ["content/content.css"],
"run_at": "document_idle"
}]
}
```

### 2. Programmatic (from service worker or popup)
```js
// Requires "scripting" permission and host access
chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['content/content.js']
});

// Or inject a function directly
chrome.scripting.executeScript({
target: { tabId: tabId },
func: (param) => {
document.body.style.backgroundColor = param;
},
args: ['yellow']
});
```

Use `activeTab` permission for on-click injection (no host_permissions needed):
```json
{
"permissions": ["activeTab", "scripting"]
}
```

## Isolated World

Content scripts run in an isolated world:
- They share the DOM with the page but NOT JavaScript variables
- They can access chrome.runtime messaging APIs
- The page's CSP does NOT restrict content script code
- `window` refers to the content script's isolated world

## Message Passing from Content Scripts

```js
// content.js → service worker
chrome.runtime.sendMessage({ type: 'DATA', payload: data }, (response) => {
console.log('Got response:', response);
});

// service worker → content script in a specific tab
chrome.tabs.sendMessage(tabId, { type: 'UPDATE', data: newData });

// content.js: listen for messages
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GET_CONTENT') {
const text = document.body.innerText;
sendResponse({ text });
}
return true; // Keep channel open for async sendResponse
});
```

## DOM Manipulation Best Practices

- **Avoid blocking the main thread** when modifying many DOM elements. Use `requestAnimationFrame`
to batch visual updates and `scheduler.yield()` to break up long-running tasks:

```js
// ❌ BAD: Blocks the main thread while processing hundreds of elements
const emails = document.body.innerText.match(/[\w.+-]+@[\w-]+\.[\w.]+/g);
emails.forEach(email => {
// ... find and highlight each email (can freeze the page)
});

// ✅ GOOD: Process in batches using requestAnimationFrame
async function highlightEmails(elements) {
const BATCH_SIZE = 20;
for (let i = 0; i < elements.length; i += BATCH_SIZE) {
const batch = elements.slice(i, i + BATCH_SIZE);
await new Promise(resolve => requestAnimationFrame(() => {
batch.forEach(el => el.style.backgroundColor = 'yellow');
resolve();
}));
// Yield to the main thread between batches
if (typeof scheduler !== 'undefined' && scheduler.yield) {
await scheduler.yield();
}
}
}
```

- Use `MutationObserver` for dynamic pages (SPAs, infinite scroll)
- Namespace your CSS classes to avoid conflicts (e.g., `myext-highlight`)
- Use Shadow DOM for complex UI injected into pages
- Clean up on removal: `chrome.runtime.onMessage` listeners persist until the content script context is destroyed
- Use `TreeWalker` or `document.createNodeIterator` instead of regex on `innerHTML` for finding text in the DOM — this is more reliable and doesn't break event listeners

## `run_at` Timing

| Value | When |
|-------|------|
| `document_start` | Before DOM is constructed (useful for blocking) |
| `document_idle` | After DOM is ready but before all resources load (default, recommended) |
| `document_end` | After DOM is complete but before images/subframes |
Loading
Loading