2025-10-21 10:11:48+04:00
This commit is contained in:
27
.claude/settings.local.json
Normal file
27
.claude/settings.local.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -44,6 +45,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
needs: lint
|
needs: lint
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -71,6 +73,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
needs: test
|
needs: test
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -137,6 +140,7 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
name: Build Docker Image
|
name: Build Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -184,6 +188,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
needs: [build, docker]
|
needs: [build, docker]
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ jobs:
|
|||||||
build-and-push:
|
build-and-push:
|
||||||
name: Build and Push Docker Image
|
name: Build and Push Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ jobs:
|
|||||||
create-release:
|
create-release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
outputs:
|
outputs:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
steps:
|
steps:
|
||||||
@@ -43,6 +44,7 @@ jobs:
|
|||||||
name: Build and Upload Assets
|
name: Build and Upload Assets
|
||||||
needs: create-release
|
needs: create-release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
|||||||
154
CLAUDE.md
Normal file
154
CLAUDE.md
Normal file
@@ -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`
|
||||||
162
HTML Theme Switcher Implementation.md
Normal file
162
HTML Theme Switcher Implementation.md
Normal file
@@ -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
|
||||||
@@ -6,6 +6,7 @@ name: "Your Name"
|
|||||||
bio: "A short bio about yourself"
|
bio: "A short bio about yourself"
|
||||||
avatar: "static/logo.png" # Can be a local path or URL (http://... or https://...)
|
avatar: "static/logo.png" # Can be a local path or URL (http://... or https://...)
|
||||||
theme: "auto"
|
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:
|
links:
|
||||||
- title: "My Website"
|
- title: "My Website"
|
||||||
url: "https://yourwebsite.com"
|
url: "https://yourwebsite.com"
|
||||||
|
|||||||
Reference in New Issue
Block a user