diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..465e11e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,27 @@ +{ + "permissions": { + "allow": [ + "Bash(dist/linkbeam:*)", + "Bash(docker build:*)", + "Bash(go run:*)", + "Bash(go test -v ./...)", + "Bash(go test:*)", + "Bash(golangci-lint help:*)", + "Bash(golangci-lint run:*)", + "Bash(make:*)", + "Bash(sudo docker build:*)", + "Bash(docker compose:*)", + "Bash(sudo docker compose build:*)", + "Bash(sudo docker compose:*)", + "Bash(./dist/linkbeam:*)", + "Bash(tree:*)", + "Read(//tmp/**)", + "Bash(cat:*)", + "Bash(for file in internal/config/testdata/config-{nord,gruvbox,catppuccin}.yaml)", + "Bash(do)", + "Bash(done)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index abf3ccd..ab22ae4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -15,6 +15,7 @@ jobs: lint: name: Lint runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest steps: - name: Checkout code uses: actions/checkout@v4 @@ -44,6 +45,7 @@ jobs: test: name: Test runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest needs: lint steps: - name: Checkout code @@ -71,6 +73,7 @@ jobs: build: name: Build runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest needs: test strategy: matrix: @@ -137,6 +140,7 @@ jobs: docker: name: Build Docker Image runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest needs: test steps: - name: Checkout code @@ -184,6 +188,7 @@ jobs: release: name: Create Release runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest needs: [build, docker] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') steps: diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml index d9af1b4..fe19ccd 100644 --- a/.gitea/workflows/docker-publish.yml +++ b/.gitea/workflows/docker-publish.yml @@ -14,6 +14,7 @@ jobs: build-and-push: name: Build and Push Docker Image runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest permissions: contents: read packages: write diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 305b480..ca9c123 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -9,6 +9,7 @@ jobs: create-release: name: Create Release runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -43,6 +44,7 @@ jobs: name: Build and Upload Assets needs: create-release runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest strategy: matrix: include: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..60deef4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,154 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +LinkBeam is a static site generator for creating personal link-in-bio pages (similar to Linktree). It reads a YAML configuration file containing user profile information, links, and social media, then generates a themed HTML page using Go templates. + +## CI/CD Pipeline + +The project uses Gitea Actions for continuous integration and deployment. + +### Workflows + +1. **CI Pipeline** (`.gitea/workflows/ci.yml`): + - Runs on push to main/develop and pull requests + - **Lint**: golangci-lint, go fmt, go vet + - **Test**: Run tests with race detection and coverage + - **Build**: Multi-platform builds (Linux, Windows, macOS) for amd64, 386, arm64, armv7 + - **Docker**: Build multi-arch Docker images + +2. **Docker Publish** (`.gitea/workflows/docker-publish.yml`): + - Triggered on version tags (v*) + - Builds and pushes Docker images to container registry + - Multi-architecture: linux/amd64, linux/arm64, linux/arm/v7 + +3. **Release** (`.gitea/workflows/release.yml`): + - Triggered on semver tags (v*.*.*) + - Creates GitHub/Gitea releases with binaries for all platforms + - Generates SHA256 checksums for verification + +### Local Testing + +```bash +# Check version +./linkbeam -v +# or: ./linkbeam --version + +# Test Docker build +docker build -t linkbeam:test . + +# Run Docker container +docker run -v $(pwd)/config.yaml:/app/config/config.yaml \ + -v $(pwd)/dist:/app/dist \ + linkbeam:test + +# Test with docker-compose +docker-compose up +``` + +## Development Commands + +### Pre-commit Setup + +This project uses pre-commit hooks for code quality. First-time setup: + +```bash +# Install pre-commit hooks +make install-hooks +# or: pre-commit install + +# Run all hooks manually +make pre-commit +# or: pre-commit run --all-files +``` + +The hooks automatically run on commit and check: `go fmt`, `goimports`, `go vet`, `golangci-lint`, `go test`, `go mod tidy`, YAML syntax, and more. + +### Building and Running +```bash +# Build binary (outputs to dist/linkbeam) +make build +# or: go build -o dist/linkbeam cmd/linkbeam/main.go + +# Run with example config +make run-example +# or: go run cmd/linkbeam/main.go --config internal/config/testdata/config.yaml + +# Run with custom config (long flags) +./linkbeam --config config.yaml --template templates/base.html --output dist/index.html + +# Run with custom config (short flags) +./linkbeam -c config.yaml -t templates/base.html -o dist/index.html + +# Run all tests +make test +# or: go test -v ./... + +# Run tests for a specific package +go test ./internal/config +go test ./internal/generator + +# Run a single test function +go test -v -run TestLoad ./internal/config + +# Clean build artifacts +make clean +``` + +### CLI Options +- `-c, --config`: Path to config YAML file (default: "config.yaml") +- `-t, --template`: Path to HTML template (default: "templates/base.html") +- `-o, --output`: Path to output HTML file (default: "dist/index.html") +- `-v, --version`: Print version information + +### Output +The `dist/` directory contains both build artifacts and generated site: +- `dist/linkbeam` - Compiled binary +- `dist/index.html` - Main HTML page +- `dist/themes/` - Copied CSS theme files + +## Architecture + +### Two-Layer Structure + +1. **Config Layer** (`internal/config/`): + - Loads and validates YAML configuration files + - Defines data structures: `Config`, `Link`, `Social` + - Validates theme (must be "light" or "dark") and required fields (name cannot be empty) + - Config validation happens automatically during `config.Load()` + +2. **Generator Layer** (`internal/generator/`): + - **HTML Generation**: Orchestrates template rendering using Go's `html/template` + - `GenerateSite(cfg, templatePath, outPath)` - Creates HTML from templates + - **Asset Management**: Copies CSS themes and static files to output directory + - `CopyAssets(distDir, themeDirs...)` - Copies theme files to dist/themes/ + - **Text Rendering**: Plain-text representation for debugging + - `RenderUserPage(cfg)` - Returns text-based preview of the page + +### Configuration Format + +The config YAML structure in `internal/config/testdata/config.yaml`: +- `name`: User's display name (required) +- `bio`: Short biography text +- `avatar`: Path to avatar image +- `theme`: "light" or "dark" (validated) +- `links[]`: Array of link objects with `title`, `url`, `icon` +- `socials[]`: Array of social media links with `platform`, `url`, `icon` + +### Templates + +Templates in `templates/` use Go template syntax: +- `base.html`: Main page template with theme support (`data-theme="{{ .Theme }}"`) +- Template receives the entire `Config` struct as data +- Access config fields with `{{ .Name }}`, `{{ .Bio }}`, etc. +- Iterate over links with `{{- range .Links }}` + +### Testing Strategy + +- Tests use table-driven patterns and dependency injection +- `cmd/linkbeam/main_test.go`: Mocks the config loader and captures stdout +- Config tests validate YAML parsing and business rules +- Generator tests verify file creation and template execution +- All tests use the testdata in `internal/config/testdata/config.yaml` diff --git a/HTML Theme Switcher Implementation.md b/HTML Theme Switcher Implementation.md new file mode 100644 index 0000000..1dcfa19 --- /dev/null +++ b/HTML Theme Switcher Implementation.md @@ -0,0 +1,162 @@ + + +## What It Does + +A three-option theme toggle (System/Light/Dark) that works reliably across all browsers. + +**Features:** +- Cycles through: System (πŸ’‘) β†’ Light (β˜€οΈ) β†’ Dark (πŸŒ™) +- Persists across page refreshes +- Follows OS theme preference when "System" is selected +- Works in private browsing mode +- Mobile-friendly (icon-only) + +## The Solution + +### CSS + +Three separate rules that work together: + +```css +/* Default: Light mode */ +:root { + --bg-primary: var(--nord-snow-storm-3); + --text-primary: var(--nord-polar-night-2); + /* ... */ +} + +/* System dark mode */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: var(--nord-polar-night-1); + --text-primary: var(--nord-snow-storm-3); + /* ... */ + } +} + +/* Manual overrides (highest priority) */ +html[data-theme="dark"] { + --bg-primary: var(--nord-polar-night-1); + --text-primary: var(--nord-snow-storm-3); + /* ... */ +} + +html[data-theme="light"] { + --bg-primary: var(--nord-snow-storm-3); + --text-primary: var(--nord-polar-night-2); + /* ... */ +} +``` + +**Why this works:** `html[data-theme]` has higher specificity than the media query, so manual selection always overrides system preference. + +### JavaScript + +```javascript +const THEME_OPTIONS = ['system', 'light', 'dark']; + +function toggleTheme() { + const current = getFromStorage('themePreference', 'system'); + const nextIndex = (THEME_OPTIONS.indexOf(current) + 1) % 3; + const next = THEME_OPTIONS[nextIndex]; + + applyTheme(next); +} + +function applyTheme(theme) { + // Update HTML attribute + if (theme === 'system') { + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', theme); + } + + // Update UI + document.getElementById('themeIcon').textContent = THEME_ICONS[theme]; + document.getElementById('themeLabel').textContent = THEME_LABELS[theme]; + + // Save preference + saveToStorage('themePreference', theme); +} + +// Initialize on load +initTheme(); +``` + +### Storage Fallback + +Works even when localStorage is blocked: + +```javascript +function getStorage() { + try { + localStorage.setItem('__test__', '1'); + localStorage.removeItem('__test__'); + return localStorage; + } catch { + return sessionStorage; // Fallback + } +} +``` + +## Why It Works + +1. **System mode**: No `data-theme` attribute β†’ media query applies +2. **Light mode**: `data-theme="light"` β†’ overrides media query +3. **Dark mode**: `data-theme="dark"` β†’ overrides media query + +## Firefox Fix + +**Problem:** Initial implementation had invalid CSS: +```css +/* ❌ Invalid */ +html[data-theme="dark"], +@media (prefers-color-scheme: dark) { ... } +``` + +**Solution:** Separate rules: +```css +/* βœ… Valid */ +@media (prefers-color-scheme: dark) { + :root { ... } +} + +html[data-theme="dark"] { ... } +``` + +## Tests + +12 unit tests verify: +- Theme storage/retrieval +- localStorage β†’ sessionStorage fallback +- All three theme options work + +Run: `npm test` + +## Available Themes + +LinkBeam includes the following themes with automatic light/dark switching: + +1. **auto** - Simple default theme +2. **nord** - Nord color palette (light/dark variants) +3. **gruvbox** - Gruvbox color palette (light/dark variants) +4. **catppuccin-latte** - Catppuccin Latte β†’ Mocha (light β†’ dark) +5. **catppuccin-frappe** - Catppuccin Latte β†’ FrappΓ© (light β†’ dark) +6. **catppuccin-macchiato** - Catppuccin Latte β†’ Macchiato (light β†’ dark) +7. **catppuccin-mocha** - Catppuccin Latte β†’ Mocha (light β†’ dark) + +All themes follow the same CSS structure with: +- Default light mode colors in `:root` +- Dark mode colors in `@media (prefers-color-scheme: dark)` +- Manual overrides via `html[data-theme="light|dark"]` + +## Files + +- `templates/base.html` - Theme switcher button and script +- `themes/*.css` - Theme CSS files with light/dark variants +- `internal/config/config.go` - Auto-discovers available themes + +--- + +**Status:** βœ… Working in all browsers +**Last Updated:** 2025-10-12 diff --git a/config.example.yaml b/config.example.yaml index 9570820..36eceb5 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,6 +6,7 @@ name: "Your Name" bio: "A short bio about yourself" avatar: "static/logo.png" # Can be a local path or URL (http://... or https://...) theme: "auto" +# font_awesome_cdn: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" # Optional: custom Font Awesome CDN URL (default: Font Awesome 6.5.1) links: - title: "My Website" url: "https://yourwebsite.com"