Connor McCutcheon
/ SkyShot
CLAUDE.md
md
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**SkyShot** is a screenshot-as-a-service microservice built with Skykit (The Skyscape's web framework). It captures full-page screenshots of websites using headless Chrome and serves them via a simple REST API.
## Architecture
### Technology Stack
- **Framework**: Skykit (local dependency via `../skykit`)
- **Screenshot Engine**: chromedp (headless Chrome automation)
- **Database**: LibSQL via Skykit's `ConnectDB()`
- **Frontend**: HTMX + Tailwind CSS + DaisyUI (dark theme)
- **Container**: Docker with Chromium
### Design Principles
1. **Simplicity**: Parse URL from path, capture screenshot, serve image
2. **Fast Timeout**: 400ms maximum capture time
3. **Graceful Degradation**: Serve default image on any error
4. **Caching**: Store screenshots in database for 24-hour reuse
5. **Microservice**: Single responsibility, lightweight, embeddable
## Project Structure
```
skyshot/
├── main.go                    # Application entry point
├── controllers/
│   ├── screenshots.go         # Screenshot capture and serving logic
│   └── default.png            # Embedded fallback image
├── models/
│   ├── database.go            # Database connection and manager setup
│   └── screenshot.go          # Screenshot model and business logic
├── views/
│   ├── home.html              # Homepage with usage docs (Lorem Picsum style)
│   └── public/
│       └── default.png        # Fallback image (web-server background.png)
├── Dockerfile                 # Docker config with Chromium
├── go.mod                     # Go dependencies (local skykit via replace)
└── README.md                  # User-facing documentation
```
## Development Commands
### Running Locally
**With Docker (recommended):**
```bash
docker build -t skyshot .
docker run -p 5000:5000 skyshot
```
**Without Docker (requires Chromium installed):**
```bash
go run .
```
### Building
```bash
go build -o skyshot .
```
### Testing
```bash
# Test screenshot endpoint
curl http://localhost:5000/https://example.com -o test.png
# Test homepage
curl http://localhost:5000
```
## Key Implementation Details
### URL Routing Pattern
The app uses a catch-all route `/{url...}` to capture the entire path as the target URL:
```go
http.HandleFunc("GET /{url...}", c.handleScreenshot)
```
This allows URLs like:
- `/https://example.com` → screenshots example.com
- `/https://github.com/user/repo` → screenshots GitHub repo
### Screenshot Capture Flow
1. **Parse URL**: Extract from path, validate has http:// or https://
2. **Check Cache**: Look up in database by URL
3. **Capture**: Use chromedp with 400ms timeout
4. **Store**: Save to database asynchronously (non-blocking)
5. **Serve**: Return image with 24-hour cache header
### Error Handling Strategy
**All errors result in default image:**
- Invalid URL format → default image
- Timeout (>400ms) → default image
- chromedp error → default image
- Network error → default image
This ensures the service never returns HTTP errors for screenshot requests.
### chromedp Integration
```go
ctx, cancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
defer cancel()
allocCtx, allocCancel := chromedp.NewContext(ctx)
defer allocCancel()
var buf []byte
err := chromedp.Run(allocCtx,
    chromedp.Navigate(targetURL),
    chromedp.WaitReady("body"),
    chromedp.FullScreenshot(&buf, 90), // 90% quality
)
```
### Database Schema
**Screenshot Model:**
```go
type Screenshot struct {
    skykit.Model              // ID, CreatedAt, UpdatedAt
    URL       string          // Unique index
    ImageData []byte          // PNG bytes
    IsDefault bool            // Fallback flag
}
```
The `URL` field has a unique index to prevent duplicate captures.
## Deployment
### Using Skykit's launch-app
```bash
# Build binary
go build -o skyshot .
# Deploy to The Skyscape infrastructure
../devtools/build/launch-app deploy \
  --name skyshot \
  --binary ./skyshot
```
### Docker Deployment
The Dockerfile installs Chromium and builds the Go binary:
```dockerfile
FROM golang:1.24-bookworm
RUN apt-get update && apt-get install -y chromium chromium-sandbox
ENV CHROME_BIN=/usr/bin/chromium
```
## Environment Variables
- `PORT` - Server port (default: 5000)
- `HOST_PREFIX` - Host prefix for routing (optional)
- `CHROME_BIN` - Path to Chromium (set in Dockerfile)
## Integration with web-server
Once deployed, web-server can use SkyShot for:
### App Screenshots
```html
<img src="https://shot.skysca.pe/{{ .App.URL }}" alt="App preview">
```
### Repository Previews
```html
<img src="https://shot.skysca.pe/https://theskyscape.com/repo/{{ .Repo.ID }}"
     alt="Repo preview">
```
### Social Cards
```html
<meta property="og:image"
      content="https://shot.skysca.pe/https://theskyscape.com/{{ .Path }}">
```
## Common Development Tasks
### Adding Query Parameters
To add width/height parameters:
1. Update `handleScreenshot` to parse query params
2. Modify `captureScreenshot` to accept dimensions
3. Update chromedp.Run with `chromedp.EmulateViewport(width, height)`
### Changing Timeout
Edit `controllers/screenshots.go:60`:
```go
imageData, err := c.captureScreenshot(parsedURL.String(), 400*time.Millisecond)
```
### Custom Fallback Image
Replace `views/public/default.png` and `controllers/default.png` with new image.
### Adding Screenshot Formats
Currently supports PNG. To add JPEG:
1. Add format query parameter
2. Use `chromedp.CaptureScreenshot()` instead of `FullScreenshot()`
3. Add JPEG encoding logic
## Troubleshooting
### Chromium Not Found
**In Docker:** Ensure Dockerfile installs `chromium` and `chromium-sandbox`
**Locally:** Install with `apt-get install chromium-browser`
### Timeout Too Aggressive
If screenshots frequently timeout, increase from 400ms:
- Edit `controllers/screenshots.go:60`
- Increase timeout value
- Consider caching strategy
### Database Issues
Check database connection:
```bash
# Database file location
ls -la ~/.skyscape/skyshot.db
```
### Default Image Not Loading
Ensure both copies exist:
- `views/public/default.png` - Served via http.ServeFile
- `controllers/default.png` - Embedded in binary
## Performance Considerations
### Caching
- Screenshots cached in database indefinitely
- Browser cache set to 24 hours
- First request is slow (~400ms), subsequent requests are instant
### Concurrency
- chromedp handles one screenshot at a time per instance
- Scale horizontally by running multiple instances
- Database writes are asynchronous (non-blocking)
### Memory
- Each screenshot ~100-500KB as PNG
- Database can grow large with many unique URLs
- Consider cleanup strategy for old screenshots
## Future Enhancements
1. **Cleanup Job**: Delete screenshots older than 30 days
2. **Custom Dimensions**: Accept width/height query params
3. **Format Support**: Add JPEG, WebP options
4. **Element Capture**: Screenshot specific CSS selectors
5. **Mobile Emulation**: Device-specific screenshots
6. **Rate Limiting**: Prevent abuse
7. **Authentication**: Require API keys for usage
## Testing Strategy
### Manual Testing
```bash
# Test homepage
curl http://localhost:5000
# Test screenshot capture
curl http://localhost:5000/https://example.com -o test.png
file test.png  # Should show: PNG image data
# Test caching (should be instant)
time curl http://localhost:5000/https://example.com -o test2.png
# Test error handling (should return default image)
curl http://localhost:5000/invalid-url -o error.png
file error.png  # Should still be valid PNG
```
### Integration Testing
Test with web-server:
```html
<!-- In web-server template -->
<img src="http://localhost:5000/https://example.com"
     alt="Screenshot test"
     width="400">
```
## Skykit Framework Patterns
This app follows standard Skykit patterns:
### Controller Pattern
```go
func Screenshots() (string, skykit.Handler) {
    return "screenshots", &ScreenshotsController{}
}
type ScreenshotsController struct {
    skykit.Controller  // Embed base controller
}
func (c *ScreenshotsController) Setup(app *skykit.Application) {
    c.Controller.Setup(app)
    http.Handle("/", c.Serve("home.html", nil))
}
func (c ScreenshotsController) Handle(r *http.Request) skykit.Handler {
    c.Request = r  // Value receiver for request isolation
    return &c
}
```
### Model Pattern
```go
type Screenshot struct {
    skykit.Model  // Embeds ID, CreatedAt, UpdatedAt
    // ... fields
}
func (s *Screenshot) GetModel() *skykit.Model {
    return &s.Model
}
```
### Database Pattern
```go
var (
    DB          = skykit.ConnectDB()
    Screenshots = skykit.Manage(DB, "screenshots", new(Screenshot))
)
func init() {
    go Screenshots.UniqueIndex("URL")
}
```
## Dependencies
### Direct Dependencies
- `theskyscape.com/repo/skykit` - Web framework (local via replace)
- `github.com/chromedp/chromedp` - Headless Chrome automation
### Indirect Dependencies (via skykit)
- LibSQL/SQLite - Database
- Go stdlib - HTTP server, templating, etc.
## Documentation References
- **Skykit Framework**: See `../skykit/` for framework documentation
- **chromedp**: https://github.com/chromedp/chromedp
- **chromedp examples**: https://github.com/chromedp/examples
- **The Skyscape**: See `../CLAUDE.md` for platform overview
## Design Inspiration
- **Lorem Picsum**: https://picsum.photos - Photo placeholder service
- **Urlbox**: https://urlbox.io - Screenshot API service
- **Microlink**: https://microlink.io - URL metadata and screenshots
No comments yet.