Install
$ agentstack add skill-jamditis-claude-skills-journalism-zero-build-frontend ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Zero-build frontend development
Patterns for building production-quality web applications without build tools, bundlers, or complex toolchains.
Picking a stack
Three current zero-build approaches, each with different trade-offs:
| Stack | When | Bundle size impact | |---|---|---| | React via esm.sh + htm | Component-heavy SPAs, existing React mental model, Tailwind styling | ~50 KB gzipped (React + ReactDOM + htm) | | htmx 2.x + server-rendered HTML | CRUD apps, traditional MPA flow, want server-side state of truth | ~14 KB gzipped (htmx alone) | | Alpine.js 3.x + plain HTML | Light interactivity sprinkled into mostly-static pages, no full SPA | ~15 KB gzipped (Alpine alone) |
You can mix htmx and Alpine.js in the same page — htmx handles server interactions, Alpine handles client-side UI state. Many production sites converge on this combo.
ESM import maps
Import maps let you write import x from 'react' in a `` without a bundler — the browser resolves the bare specifier against the map. Stable in all major browsers since 2023.
{
"imports": {
"react": "https://esm.sh/react@19.0.0",
"react-dom/client": "https://esm.sh/react-dom@19.0.0/client",
"lodash-es": "https://esm.sh/lodash-es@4.17.21",
"@my-app/": "/src/"
},
"scopes": {
"https://esm.sh/": {
"scheduler": "https://esm.sh/scheduler@0.23.0"
}
}
}
The scopes block lets a sub-tree of imports resolve differently. Useful when one CDN package needs a specific transitive dependency. The trailing / form ("@my-app/": "/src/") lets you import any file under that prefix.
Pin versions in production. esm.sh/react (without a version) and esm.sh/react@latest resolve at request time and can shift under you. Use exact pinned versions or SHA-locked URLs.
htmx 2.x — server-rendered interactivity
htmx 2.0 (released June 2024) lets you add AJAX, WebSockets, and SSE to plain HTML through hx-* attributes. The server sends HTML fragments; the client swaps them in. No JS framework required.
Click me
...
htmx 2.x dropped IE support and tightened the API; if you're on htmx 1.x and don't need to migrate, 1.x still receives security patches. New code should target 2.x.
Alpine.js 3.x — client-side reactivity in HTML
Alpine.js (current 3.14+) is a minimal alternative to Vue/React for sprinkles of interactivity. State and behavior live as x-* attributes in the markup.
Toggle
Content here
Alpine pairs naturally with htmx: htmx swaps a server-rendered fragment in, Alpine handles whatever client-side state that fragment needs (open/close, optimistic toggles, form validation).
React via CDN (esm.sh)
Basic setup
Zero-Build React App
tailwind.config = {
theme: {
extend: {
fontFamily: {
display: ['Special Elite', 'monospace'],
body: ['Roboto Mono', 'monospace'],
},
colors: {
brand: {
primary: '#2dc8d2',
secondary: '#f34213',
dark: '#183642',
}
}
}
}
}
{
"imports": {
"react": "https://esm.sh/react@19.0.0",
"react-dom/client": "https://esm.sh/react-dom@19.0.0/client",
"htm": "https://esm.sh/htm@3.1.1"
}
}
React with htm (no JSX, no build)
// index.js
import React, { useState, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import htm from 'htm';
// Bind htm to React.createElement
const html = htm.bind(React.createElement);
// Components use html`` instead of JSX
function App() {
const [records, setRecords] = useState([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
useEffect(() => {
loadData();
}, []);
async function loadData() {
try {
const response = await fetch('data/archive-data.json');
const data = await response.json();
setRecords(data.records);
} catch (error) {
console.error('Failed to load data:', error);
} finally {
setLoading(false);
}
}
const filtered = records.filter(r =>
r.title.toLowerCase().includes(search.toLowerCase())
);
if (loading) {
return html`
`;
}
return html`
Archive Explorer
setSearch(e.target.value)}
class="mt-2 w-full p-2 bg-gray-800 rounded border border-gray-600 focus:border-brand-primary outline-none"
/>
`;
}
function RecordList({ records }) {
return html`
${records.map(record => html`
`)}
`;
}
function RecordCard({ record }) {
return html`
${record.title}
${record.publication_date}
${record.summary}
${record.tags?.map(tag => html`
${tag}
`)}
`;
}
// Mount app
const root = createRoot(document.getElementById('root'));
root.render(html``);
Data caching with localStorage
// services/cacheService.js
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
export function getCached(key) {
const cached = localStorage.getItem(key);
if (!cached) return null;
try {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp > CACHE_TTL) {
localStorage.removeItem(key);
return null;
}
return data;
} catch {
localStorage.removeItem(key);
return null;
}
}
export function setCache(key, data) {
localStorage.setItem(key, JSON.stringify({
data,
timestamp: Date.now()
}));
}
export async function fetchWithCache(url, cacheKey) {
// Check cache first
const cached = getCached(cacheKey);
if (cached) return cached;
// Fetch fresh data
const response = await fetch(url);
const data = await response.json();
// Cache for next time
setCache(cacheKey, data);
return data;
}
// Usage
const records = await fetchWithCache('data/archive-data.json', 'archive-records');
Leaflet.js maps
Basic map setup
#map { height: 85vh; width: 100%; }
Map application with clustering
// js/app.js
class MapApp {
constructor() {
this.map = null;
this.markers = null;
this.data = [];
this.filters = {
year: null,
county: null,
status: null
};
}
async init() {
this.setupMap();
await this.loadData();
this.renderMarkers();
this.setupFilters();
}
setupMap() {
// Initialize map centered on NJ
this.map = L.map('map', {
center: [40.0583, -74.4057],
zoom: 8,
scrollWheelZoom: false, // Disable mouse wheel zoom
zoomControl: false // We'll add custom controls
});
// Add tile layer (CARTO Voyager)
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap, © CARTO',
maxZoom: 19
}).addTo(this.map);
// Add custom zoom control (top-right)
L.control.zoom({ position: 'topright' }).addTo(this.map);
// Initialize marker cluster group
this.markers = L.markerClusterGroup({
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
maxClusterRadius: 50,
spiderLegPolylineOptions: { weight: 1.5, color: '#2dc8d2' }
});
this.map.addLayer(this.markers);
}
async loadData() {
const response = await fetch('data/grantees.json');
this.data = await response.json();
}
renderMarkers() {
this.markers.clearLayers();
const filtered = this.data.filter(item => {
if (this.filters.year && item.year !== this.filters.year) return false;
if (this.filters.county && item.county !== this.filters.county) return false;
if (this.filters.status && item.status !== this.filters.status) return false;
return true;
});
filtered.forEach(item => {
if (!item.lat || !item.lng) return;
const marker = L.marker([item.lat, item.lng], {
icon: this.createIcon(item.status)
});
marker.bindPopup(this.createPopup(item));
this.markers.addLayer(marker);
});
// Update count display
document.getElementById('count').textContent = filtered.length;
}
createIcon(status) {
const colors = {
'Active': '#2dc8d2',
'Completed': '#666666',
'Pending': '#f34213'
};
return L.divIcon({
html: ``,
className: 'custom-marker',
iconSize: [16, 16],
iconAnchor: [8, 8]
});
}
createPopup(item) {
return `
${item.name}
${item.county} County
${item.description || ''}
${item.status}
${item.year}
${item.website ? `Visit Website →` : ''}
`;
}
setupFilters() {
// Year filter
const years = [...new Set(this.data.map(d => d.year))].sort();
const yearSelect = document.getElementById('year-filter');
years.forEach(year => {
const option = document.createElement('option');
option.value = year;
option.textContent = year;
yearSelect.appendChild(option);
});
yearSelect.addEventListener('change', (e) => {
this.filters.year = e.target.value || null;
this.renderMarkers();
});
// Similar for county, status filters...
}
}
// Initialize on load
document.addEventListener('DOMContentLoaded', () => {
const app = new MapApp();
app.init();
});
Google Sheets as database
Fetching published CSV
// Google Sheets published as CSV
const SHEET_URL = 'https://docs.google.com/spreadsheets/d/e/SPREADSHEET_ID/pub?gid=0&single=true&output=csv';
async function loadFromSheets() {
const response = await fetch(SHEET_URL);
const csv = await response.text();
// Parse with PapaParse (CDN)
const { data, errors } = Papa.parse(csv, {
header: true,
skipEmptyLines: true,
transformHeader: (h) => h.trim().toLowerCase().replace(/\s+/g, '_')
});
if (errors.length > 0) {
console.warn('CSV parsing errors:', errors);
}
return data;
}
Real-time state with localStorage
class DataManager {
constructor(sheetUrl, cacheKey) {
this.sheetUrl = sheetUrl;
this.cacheKey = cacheKey;
this.data = [];
this.localState = this.loadLocalState();
}
loadLocalState() {
const stored = localStorage.getItem(`${this.cacheKey}-state`);
return stored ? JSON.parse(stored) : {};
}
saveLocalState() {
localStorage.setItem(`${this.cacheKey}-state`, JSON.stringify(this.localState));
}
async refresh() {
const response = await fetch(this.sheetUrl);
const csv = await response.text();
this.data = Papa.parse(csv, { header: true, skipEmptyLines: true }).data;
// Merge with local state
this.data.forEach(row => {
const localData = this.localState[row.id];
if (localData) {
Object.assign(row, localData);
}
});
return this.data;
}
updateLocal(id, updates) {
this.localState[id] = { ...this.localState[id], ...updates };
this.saveLocalState();
// Update in-memory data too
const item = this.data.find(d => d.id === id);
if (item) Object.assign(item, updates);
}
}
// Usage
const manager = new DataManager(SHEET_URL, 'volunteer-data');
await manager.refresh();
// Mark task as complete (stored locally)
manager.updateLocal('task-123', { completed: true, completed_at: new Date().toISOString() });
Browser extension (Manifest V3)
manifest.json
{
"manifest_version": 3,
"name": "PocketLink",
"version": "1.0.0",
"description": "Create shortlinks from right-click context menu",
"permissions": [
"contextMenus",
"storage",
"activeTab",
"scripting",
"notifications",
"offscreen"
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"options_page": "options.html",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
Service worker (background.js)
// background.js - Service Worker
// Create context menu on install
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'create-shortlink',
title: 'Create Shortlink',
contexts: ['page', 'link']
});
});
// Handle context menu click
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== 'create-shortlink') return;
const url = info.linkUrl || info.pageUrl;
try {
const shortUrl = await createShortlink(url);
await copyToClipboard(shortUrl);
showNotification('Shortlink Created', shortUrl);
} catch (error) {
showNotification('Error', error.message);
}
});
async function createShortlink(longUrl) {
const { apiToken } = await chrome.storage.sync.get('apiToken');
if (!apiToken) throw new Error('API token not configured');
const response = await fetch('https://api-ssl.bitly.com/v4/shorten', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ long_url: longUrl })
});
if (!response.ok) throw new Error('API request failed');
const data = await response.json();
return data.link;
}
// Clipboard methods (three fallback strategies)
// Method 1: Offscreen API (preferred)
async function copyToClipboard(text) {
try {
await copyViaOffscreen(text);
} catch {
try {
await copyViaContentScript(text);
} catch {
await copyViaPopup(text);
}
}
}
async function copyViaOffscreen(text) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['CLIPBOARD'],
justification: 'Copy shortlink to clipboard'
});
await chrome.runtime.sendMessage({ type: 'copy', text });
await chrome.offscreen.closeDocument();
}
async function copyViaContentScript(text) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (text) => navigator.clipboard.writeText(text),
args: [text]
});
}
function showNotification(title, message) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title,
message
});
}
Options page
/* Inline CSS for extension compliance (no remote code) */
body {
font-family: system-ui, sans-serif;
padding: 20px;
max-width: 400px;
margin: 0 auto;
}
h1 { font-size: 1.5rem; margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
button {
margin-top: 1rem;
padding: 10px 20px;
background: #2dc8d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover { background: #25a8b0; }
.status { margin-top: 1rem; padding: 10px; border-radius: 4px; }
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
PocketLink Settings
Bit.ly API Token
Save Settings
// options.js
document.addEventListener('DOMContentLoaded', async () => {
const tokenInput = document.getElementById('apiToken');
const saveButton = document.getElementById('save');
const status = document.getElementById('status');
// Load saved token
const { apiToken } = await chrome.storage.sync.get('apiToken');
if (apiToken) tokenInput.value =
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [jamditis](https://github.com/jamditis)
- **Source:** [jamditis/claude-skills-journalism](https://github.com/jamditis/claude-skills-journalism)
- **License:** MIT
- **Homepage:** https://skills.amditis.tech/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.