Share HTML files, visualizations, and interactive demos publicly via Cloudflare Tunnel with live reload support.
The artifacts server lets Mom create HTML/JS/CSS files that you can instantly view in a browser, with WebSocket-based live reload for development. Perfect for dashboards, visualizations, prototypes, and interactive demos.
Node.js packages:
cd /workspace/artifacts
npm init -y
npm install express ws chokidarCloudflared (Cloudflare Tunnel):
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
cloudflared --versionSave this as /workspace/artifacts/server.js:
#!/usr/bin/env node
const express = require('express');
const { WebSocketServer } = require('ws');
const chokidar = require('chokidar');
const path = require('path');
const fs = require('fs');
const http = require('http');
const PORT = 8080;
const FILES_DIR = path.join(__dirname, 'files');
// Ensure files directory exists
if (!fs.existsSync(FILES_DIR)) {
fs.mkdirSync(FILES_DIR, { recursive: true });
}
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server, clientTracking: true });
// Track connected WebSocket clients
const clients = new Set();
// WebSocket connection handler with error handling
wss.on('connection', (ws) => {
console.log('WebSocket client connected');
clients.add(ws);
ws.on('error', (err) => {
console.error('WebSocket client error:', err.message);
clients.delete(ws);
});
ws.on('close', () => {
console.log('WebSocket client disconnected');
clients.delete(ws);
});
});
wss.on('error', (err) => {
console.error('WebSocket server error:', err.message);
});
// Watch for file changes
const watcher = chokidar.watch(FILES_DIR, {
persistent: true,
ignoreInitial: true,
depth: 99, // Watch all subdirectory levels
ignorePermissionErrors: true,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50
}
});
watcher.on('all', (event, filepath) => {
console.log(`File ${event}: ${filepath}`);
// If a new directory is created, explicitly watch it
// This ensures newly created artifact folders are monitored without restart
if (event === 'addDir') {
watcher.add(filepath);
console.log(`Now watching directory: ${filepath}`);
}
const relativePath = path.relative(FILES_DIR, filepath);
const message = JSON.stringify({
type: 'reload',
file: relativePath
});
clients.forEach(client => {
if (client.readyState === 1) {
try {
client.send(message);
} catch (err) {
console.error('Error sending to client:', err.message);
clients.delete(client);
}
} else {
clients.delete(client);
}
});
});
watcher.on('error', (err) => {
console.error('File watcher error:', err.message);
});
// Cache-busting headers
app.use((req, res, next) => {
res.set({
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
'Surrogate-Control': 'no-store'
});
next();
});
// Inject live reload script for HTML files with ?ws=true
app.use((req, res, next) => {
if (!req.path.endsWith('.html') || req.query.ws !== 'true') {
return next();
}
const filePath = path.join(FILES_DIR, req.path);
// Security: Prevent path traversal attacks
const resolvedPath = path.resolve(filePath);
const resolvedBase = path.resolve(FILES_DIR);
if (!resolvedPath.startsWith(resolvedBase)) {
return res.status(403).send('Forbidden: Path traversal detected');
}
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
return next();
}
const liveReloadScript = `
<script>
(function() {
const errorDiv = document.createElement('div');
errorDiv.style.cssText = 'position:fixed;bottom:10px;left:10px;background:rgba(0,150,0,0.9);color:white;padding:15px;border-radius:8px;font-family:monospace;font-size:12px;max-width:90%;z-index:9999;word-break:break-all';
errorDiv.textContent = 'Live reload: connecting...';
document.body.appendChild(errorDiv);
function showStatus(msg, isError) {
errorDiv.textContent = msg;
errorDiv.style.background = isError ? 'rgba(255,0,0,0.9)' : 'rgba(0,150,0,0.9)';
if (!isError) setTimeout(() => errorDiv.style.display = 'none', 3000);
}
try {
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const wsUrl = protocol + window.location.host;
const ws = new WebSocket(wsUrl);
ws.onopen = () => showStatus('Live reload connected!', false);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'reload') {
showStatus('Reloading...', false);
setTimeout(() => location.reload(), 500);
}
};
ws.onerror = () => showStatus('Connection failed', true);
ws.onclose = (e) => showStatus('Disconnected: ' + e.code, true);
} catch (err) {
showStatus('Error: ' + err.message, true);
}
})();
</script>`;
if (data.includes('</body>')) {
data = data.replace('</body>', liveReloadScript + '</body>');
} else {
data = data + liveReloadScript;
}
res.type('html').send(data);
});
});
// Serve static files
app.use(express.static(FILES_DIR));
// Error handling
app.use((err, req, res, next) => {
console.error('Express error:', err.message);
res.status(500).send('Internal server error');
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${PORT} is already in use`);
process.exit(1);
} else {
console.error('Server error:', err.message);
}
});
// Global error handlers
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing gracefully');
watcher.close();
server.close(() => process.exit(0));
});
process.on('SIGINT', () => {
console.log('SIGINT received, closing gracefully');
watcher.close();
server.close(() => process.exit(0));
});
// Start server
server.listen(PORT, () => {
console.log(`Artifacts server running on http://localhost:${PORT}`);
console.log(`Serving files from: ${FILES_DIR}`);
console.log(`Add ?ws=true to any URL for live reload`);
});Make executable:
chmod +x /workspace/artifacts/server.jsSave this as /workspace/artifacts/start-server.sh:
#!/bin/sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
echo "Starting artifacts server..."
# Start Node.js server in background
node server.js > /tmp/server.log 2>&1 &
NODE_PID=$!
# Wait for server to be ready
sleep 2
# Start cloudflare tunnel
echo "Starting Cloudflare Tunnel..."
cloudflared tunnel --url http://localhost:8080 2>&1 | tee /tmp/cloudflared.log &
TUNNEL_PID=$!
# Wait for tunnel to establish
sleep 5
# Extract and display public URL
PUBLIC_URL=$(grep -o 'https://.*\.trycloudflare\.com' /tmp/cloudflared.log | head -1)
if [ -n "$PUBLIC_URL" ]; then
echo ""
echo "=========================================="
echo "Artifacts server is running!"
echo "=========================================="
echo "Public URL: $PUBLIC_URL"
echo "Files directory: $SCRIPT_DIR/files/"
echo ""
echo "Add ?ws=true to any URL for live reload"
echo "Example: $PUBLIC_URL/test.html?ws=true"
echo "=========================================="
echo ""
echo "$PUBLIC_URL" > /tmp/artifacts-url.txt
else
echo "Warning: Could not extract public URL"
fi
# Keep script running
cleanup() {
echo "Shutting down..."
kill $NODE_PID 2>/dev/null || true
kill $TUNNEL_PID 2>/dev/null || true
exit 0
}
trap cleanup INT TERM
wait $NODE_PID $TUNNEL_PIDMake executable:
chmod +x /workspace/artifacts/start-server.sh/workspace/artifacts/
├── server.js # Node.js server
├── start-server.sh # Startup script
├── package.json # Dependencies
├── node_modules/ # Installed packages
└── files/ # PUT YOUR ARTIFACTS HERE
├── 2025-12-14-demo/
│ ├── index.html
│ ├── style.css
│ └── logo.png
├── 2025-12-15-chart/
│ └── index.html
└── test.html (standalone OK)
cd /workspace/artifacts
./start-server.shThis will:
- Start Node.js server on localhost:8080
- Create Cloudflare Tunnel with public URL
- Print the URL (e.g.,
https://random-words-123.trycloudflare.com) - Save URL to
/tmp/artifacts-url.txt
Note: URL changes every time you restart (free Cloudflare Tunnel limitation).
Folder organization:
- Create one subfolder per artifact:
$(date +%Y-%m-%d)-description/ - Put main file as
index.htmlfor clean URLs - Include images, CSS, JS, data in same folder
- CDN resources (Tailwind, Three.js, etc.) work fine
Example:
mkdir -p /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard
cat > /workspace/artifacts/files/$(date +%Y-%m-%d)-dashboard/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-900 text-white p-8">
<h1 class="text-4xl font-bold">My Dashboard</h1>
<img src="logo.png" alt="Logo">
</body>
</html>
EOFAccess:
- Development (live reload):
https://your-url.trycloudflare.com/2025-12-14-dashboard/index.html?ws=true - Share (static):
https://your-url.trycloudflare.com/2025-12-14-dashboard/index.html
When viewing with ?ws=true:
- You'll see a green box at bottom-left: "Live reload connected!"
- Edit any file in the artifact folder
- Page auto-reloads within 1 second
- Perfect for iterating on designs
Remove ?ws=true when sharing - no WebSocket overhead for viewers.
## Artifacts Server
Public web server for sharing HTML files and interactive visualizations.
**Creating artifacts:**
- Create one subfolder per artifact in `/workspace/artifacts/files/`
- Folder naming: `YYYY-MM-DD-description/` - use `$(date +%Y-%m-%d)` to get current date
- Example: `$(date +%Y-%m-%d)-threejs-demo/` → `2025-12-14-threejs-demo/`
- Put HTML, images, CSS, JS in that folder
- Main HTML file should be `index.html` for clean URLs
- CDN resources (Tailwind, Three.js, etc.) work fine - just link in HTML
- Server must be running (`cd /workspace/artifacts && ./start-server.sh`)
**Sharing artifacts:**
- **IMPORTANT:** Always use full `index.html` path for live reload to work
- URL pattern: `https://random-words-1234.trycloudflare.com/2025-12-14-threejs-demo/index.html`
- For live development: Add `?ws=true` - auto-reloads when files change
- For static sharing: Share URL without `?ws=true` - no WebSocket connection
- **Note:** Folder URLs (`/folder/`) won't inject WebSocket script, must use `/folder/index.html`
- Example: `https://random-words-1234.trycloudflare.com/2025-12-14-demo/?ws=true`
**File organization:**/workspace/artifacts/files/ ├── 2025-12-14-threejs-demo/ │ ├── index.html │ ├── style.css │ └── image.png ├── 2025-12-15-dashboard/ │ ├── index.html │ └── data.json └── test.html (standalone files OK too)
**URL changes:** Random subdomain changes on each server restart (free Cloudflare Tunnel)
## Artifacts Server
Public web server for sharing HTML artifacts and files.
**Location:** `/workspace/artifacts/`
**Structure:**
- `server.js` - Node.js server with live reload via WebSocket
- `files/` - Put all artifacts here (HTML, JS, CSS, images, etc.)
- `start-server.sh` - Startup script
- `package.json` - Node dependencies (express, ws, chokidar)
**Installed:**
- Node.js packages: express, ws, chokidar (in /workspace/artifacts/)
- cloudflared: /usr/local/bin/cloudflared (binary download)
**How to start:**
```bash
cd /workspace/artifacts
./start-server.shHow it works:
- Node server runs on localhost:8080, serves files from
/workspace/artifacts/files/ - Cloudflare Tunnel creates public HTTPS URL (random subdomain, changes on restart)
- File watcher detects changes in
files/directory - WebSocket notifies connected clients (with ?ws=true) to reload
Usage:
- Put HTML/files in
/workspace/artifacts/files/ - Access via public URL printed on startup
- Add
?ws=trueto URL for live reload:https://xxx.trycloudflare.com/test.html?ws=true - Share without
?ws=truefor static view (no WebSocket connection)
Public URL:
- Changes every time server restarts (random cloudflare subdomain)
- Current URL saved to
/tmp/artifacts-url.txt
Auto-start: Not configured. Run manually when needed or add to container startup script.
## Troubleshooting
**502 Bad Gateway:**
- Node server crashed. Check logs: `cat /tmp/server.log`
- Restart: `cd /workspace/artifacts && node server.js &`
**WebSocket not connecting:**
- Check browser console for errors
- Ensure `?ws=true` is in URL
- Red/yellow box at bottom-left shows connection errors
**Files not updating:**
- Check file watcher logs: `tail /tmp/server.log`
- Ensure files are in `/workspace/artifacts/files/`
**Port already in use:**
- Kill existing server: `pkill node`
- Wait 2 seconds, restart
**Browser caching issues:**
- Server sends no-cache headers
- Hard refresh: Ctrl+Shift+R
- Add version parameter: `?ws=true&v=2`
## Example Session
**You:** "Create a Three.js spinning cube demo with Tailwind UI"
**Mom creates:**
/workspace/artifacts/files/2025-12-14-threejs-cube/ ├── index.html (Three.js from CDN, Tailwind from CDN) └── screenshot.png
**Access:** `https://concepts-rome-123.trycloudflare.com/2025-12-14-threejs-cube/?ws=true`
**You:** "Make the cube purple and add a grid"
**Mom:** Edits `index.html`
**Result:** Your browser auto-reloads, showing purple cube with grid (within 1 second)
---
**Created for Mom users** - Your AI assistant that builds interactive web content on demand.