Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ doc/
build/

.obsidian/

# Comments server secrets and data
comments-server/.env
comments-server/data/
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ Output to build/ (dev) or deploy/ (prod)
- `ultima.lua` - Main entry point (CLI args, orchestration)
- `config.lua` - TOML configuration loader
- `lock_files.lua` - Change tracking/cache system
- `templates/engine.lua` - Custom templating engine
- `template_engine.lua` - Custom templating engine
- `pandoc/processor.lua` - Pandoc filter for frontmatter
- `templates/` - Shared default templates (fallback for sites)
- `utils/` - File I/O, formatters, functional utilities

- `sites/{site}/` - Site-specific content
Expand All @@ -59,7 +60,7 @@ Output to build/ (dev) or deploy/ (prod)
Templates use `.htmlua` extension with custom syntax (all expressions end with `}}`):
- `{{ expression }}` - Variable/expression output
- `{% lua code }}` - Lua control flow (if/for/end)
- Escaping: `\{` for literal braces
- Escaping: `\{` for literal opening braces (closing braces `}` don't need escaping)

Template context includes: `config`, `content`, `metadata`, `title`, `links`, `generate_absolute_path()`

Expand Down Expand Up @@ -87,3 +88,9 @@ Each site deploys via git subtree to a separate branch (`deploy-{site}`). The ma
- Lock files track file checksums to avoid unnecessary rewrites; use `-f` flag to force rebuild
- `content_type: "media"` references static files instead of rendering markdown
- `draft: true` hides content in production builds

## Documentation Conventions

- Use first person plural ("we", "our") rather than second person ("you", "your")
- Keep documentation concise and focused on implementation details
- Place project documentation in `docs/` directory
96 changes: 96 additions & 0 deletions comments-server/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Remark42 Comment Server
# Replace {$REMARK_DOMAIN} with your actual domain (e.g., comments.dupe.sh)
# Or set the REMARK_DOMAIN environment variable

{
auto_https off
}

:80 {
# CORS headers applied to ALL responses (including redirects)
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers "Content-Type, Authorization"
defer
}

# Serve site-specific CSS based on Referer header containing site_id
@dupe_sh {
header_regexp Referer site_id=dupe_sh
path /web/remark.css
}
route @dupe_sh {
header Content-Type "text/css; charset=utf-8"
root * /srv/static
rewrite * /theme-dupe_sh.css
file_server
}

@rorschach {
header_regexp Referer site_id=rorschach
path /web/remark.css
}
route @rorschach {
header Content-Type "text/css; charset=utf-8"
root * /srv/static
rewrite * /theme-rorschach.css
file_server
}

@whwg {
header_regexp Referer site_id=whwg
path /web/remark.css
}
route @whwg {
header Content-Type "text/css; charset=utf-8"
root * /srv/static
rewrite * /theme-whwg.css
file_server
}

# Default theme fallback for unknown sites
route /web/remark.css {
header Content-Type "text/css; charset=utf-8"
root * /srv/static
rewrite * /theme-default.css
file_server
}

route /web/last-comments.css {
header Content-Type "text/css; charset=utf-8"
root * /srv/static
rewrite * /last-comments-custom.css
file_server
}

route /web-orig/*.css {
uri replace /web-orig/ /web/
reverse_proxy remark42:8080 {
header_down Content-Type "text/css; charset=utf-8"
header_down -X-Content-Type-Options
}
}

route /web-orig/* {
uri replace /web-orig/ /web/
reverse_proxy remark42:8080
}

route /static/* {
root * /srv
file_server
}

reverse_proxy remark42:8080 {
# Override CSP to allow fonts from any source
header_down Content-Security-Policy "default-src 'none'; base-uri 'none'; form-action 'none'; connect-src 'self'; frame-src 'self' mailto:; img-src *; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src * data:; object-src 'none'; frame-ancestors *;"
}

log {
output file /data/access.log {
roll_size 10mb
roll_keep 3
}
}
}
152 changes: 152 additions & 0 deletions comments-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Remark42 Comment Server

Self-hosted comment system for Ultima-powered sites.

## Prerequisites

- A VPS or server with Docker and Docker Compose installed
- A domain/subdomain pointed to the server (e.g., `comments.dupe.sh`)
- (Optional) GitHub OAuth app for GitHub login

## Quick Start

### 1. Copy configuration

```bash
cp .env.example .env
```

### 2. Edit `.env` with your settings

Required settings:
- `REMARK_URL` - Full URL where comments will be hosted (e.g., `https://comments.dupe.sh`)
- `REMARK_DOMAIN` - Domain without protocol (e.g., `comments.dupe.sh`)
- `SECRET` - Random string for JWT signing (generate with `openssl rand -base64 32`)
- `SITE_ID` - Site identifier(s), comma-separated for multiple sites
- `ADMIN_SHARED_ID` - Your admin user ID (see below)

### 3. Set up GitHub OAuth (recommended)

1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
2. Create a new OAuth App:
- **Application name**: Your site comments
- **Homepage URL**: `https://comments.dupe.sh`
- **Authorization callback URL**: `https://comments.dupe.sh/auth/github/callback`
3. Copy the Client ID and Client Secret to your `.env` file

### 4. Find your GitHub user ID

```bash
curl -s https://api.github.com/users/YOUR_USERNAME | jq .id
```

Use this as `ADMIN_SHARED_ID` with format: `github_YOURID`

### 5. Start the server

```bash
docker compose up -d
```

### 6. Verify it's running

```bash
# Check container status
docker compose ps

# Check logs
docker compose logs -f remark42

# Test the API
curl https://comments.dupe.sh/api/v1/config
```

## Configuration

### Multiple Sites

To support multiple Ultima sites, use comma-separated site IDs:

```env
SITE_ID=dupe_sh,rorschach,whwg
```

Each site uses its own `site_id` in the Ultima `config.toml`.

### Authentication Options

| Provider | Environment Variables |
|----------|----------------------|
| GitHub | `AUTH_GITHUB_CID`, `AUTH_GITHUB_CSEC` |
| Anonymous | `AUTH_ANON=true` |
| Email | `AUTH_EMAIL_ENABLE=true`, SMTP settings |

### Notifications

Set `NOTIFY_TYPE` to receive notifications for new comments:

- `telegram` - Requires `NOTIFY_TELEGRAM_TOKEN` and `NOTIFY_TELEGRAM_CHAN`
- `email` - Uses SMTP settings
- `slack` - Requires webhook URL
- `none` - Disabled (default)

## Maintenance

### Backup data

```bash
# Stop containers
docker compose stop

# Backup data directory
tar -czvf remark42-backup-$(date +%Y%m%d).tar.gz data/

# Restart
docker compose start
```

### Update Remark42

```bash
docker compose pull
docker compose up -d
```

### View logs

```bash
docker compose logs -f remark42
docker compose logs -f caddy
```

## Admin Interface

Access the admin panel at: `https://comments.dupe.sh/web/admin`

You must be logged in with an account matching `ADMIN_SHARED_ID`.

## Troubleshooting

### SSL certificate issues

Caddy automatically obtains Let's Encrypt certificates. If issues occur:

```bash
# Check Caddy logs
docker compose logs caddy

# Ensure port 80 and 443 are open
# Ensure DNS is properly configured
```

### CORS errors

The Caddyfile includes CORS headers. If you see CORS errors, verify:
1. The `REMARK_URL` matches the actual URL
2. Your site's domain is accessible

### Comments not loading

1. Check browser console for errors
2. Verify `site_id` matches between Remark42 and Ultima config
3. Check Remark42 logs: `docker compose logs remark42`
61 changes: 61 additions & 0 deletions comments-server/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
services:
remark42:
image: umputun/remark42:latest
container_name: remark42
restart: unless-stopped
environment:
- REMARK_URL=${REMARK_URL}
- SECRET=${SECRET}
- SITE=${SITE_ID}
- ADMIN_SHARED_ID=${ADMIN_SHARED_ID}
# Auth providers (uncomment and configure as needed)
- AUTH_GITHUB_CID=${AUTH_GITHUB_CID:-}
- AUTH_GITHUB_CSEC=${AUTH_GITHUB_CSEC:-}
# Anonymous comments (disable in production if unwanted)
- AUTH_ANON=${AUTH_ANON:-true}
# Email auth (optional)
- AUTH_EMAIL_ENABLE=${AUTH_EMAIL_ENABLE:-false}
- SMTP_HOST=${SMTP_HOST:-}
- SMTP_PORT=${SMTP_PORT:-}
- SMTP_USERNAME=${SMTP_USERNAME:-}
- SMTP_PASSWORD=${SMTP_PASSWORD:-}
- SMTP_TLS=${SMTP_TLS:-true}
- AUTH_EMAIL_FROM=${AUTH_EMAIL_FROM:-}
# Notifications (optional)
- NOTIFY_TYPE=${NOTIFY_TYPE:-none}
- NOTIFY_TELEGRAM_TOKEN=${NOTIFY_TELEGRAM_TOKEN:-}
- NOTIFY_TELEGRAM_CHAN=${NOTIFY_TELEGRAM_CHAN:-}
# Moderation
- ADMIN_SHARED_EMAIL=${ADMIN_SHARED_EMAIL:-}
# Rate limiting
- MAX_COMMENT_SIZE=2048
- LOW_SCORE=-5
- CRITICAL_SCORE=-10
volumes:
- ./data:/srv/var:Z
ports:
- "127.0.0.1:8080:8080"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "8081:80"
- "8443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro,Z
- ./static:/srv/static:ro,Z
- caddy_data:/data
- caddy_config:/config
depends_on:
- remark42

volumes:
caddy_data:
caddy_config:
3 changes: 3 additions & 0 deletions comments-server/static/BerkeleyMono-Regular.woff
Git LFS file not shown
3 changes: 3 additions & 0 deletions comments-server/static/BerkeleyMono-Regular.woff2
Git LFS file not shown
7 changes: 7 additions & 0 deletions comments-server/static/last-comments-custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* Import original Remark42 last-comments styles */
@import '/web-orig/last-comments.css';

/* Custom overrides */
body {
font-family: 'berkeley_mono_regular', ui-monospace, monospace;
}
Loading