diff --git a/.env.dist b/.env.dist index f75d0e0..27c88cd 100644 --- a/.env.dist +++ b/.env.dist @@ -19,7 +19,11 @@ APP_ENV=dev APP_SECRET=9e287f1ad737386dde46d51e80487236 ###< symfony/framework-bundle ### ###> docker ### -SERVER_NAME=localhost +# Dev URL: http://127.0.0.1:${HTTP_PORT}/ (override HTTP_PORT/HTTPS_PORT if busy). +HTTP_PORT=9080 +HTTPS_PORT=9443 +# SERVER_NAME=:80 +# If MYSQL_* changed after the DB volume exists: docker compose down -v (wipes data), then up. MYSQL_DATABASE=unfold_db MYSQL_VERSION=8.0 MYSQL_CHARSET=utf8mb4 diff --git a/assets/laeserin.png b/assets/laeserin.png new file mode 100644 index 0000000..b72758f Binary files /dev/null and b/assets/laeserin.png differ diff --git a/assets/laeserin_icon.png b/assets/laeserin_icon.png new file mode 100644 index 0000000..b63addd Binary files /dev/null and b/assets/laeserin_icon.png differ diff --git a/assets/laeserin_logo.png b/assets/laeserin_logo.png new file mode 100644 index 0000000..033e342 Binary files /dev/null and b/assets/laeserin_logo.png differ diff --git a/assets/styles/app.css b/assets/styles/app.css index 7c0503f..07f0089 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -29,6 +29,10 @@ h1.brand { font-family: var(--brand-font), serif; font-size: 3.6rem; color: var(--brand-color); + display: inline-flex; + align-items: center; + gap: 0.45em; + line-height: 1.05; } @@ -218,6 +222,7 @@ div:nth-child(odd) .featured-list { .truncate { display: -webkit-box; -webkit-line-clamp: 3; /* limit to 3 lines */ + line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; @@ -297,9 +302,32 @@ div:nth-child(odd) .featured-list { font-weight: normal; } -.header__logo img { +/* Fixed square + overflow clips to a true circle. Logo img is out-of-flow so + global img { height: auto } cannot shrink the bitmap; object-fit fills the disc. + Slight scale crops typical padding baked into square marketing PNGs. */ +.header__logo-circle { + display: inline-block; + position: relative; + width: 60px; height: 60px; - vertical-align: bottom; + flex-shrink: 0; + border-radius: 50%; + overflow: hidden; + box-shadow: 0 0 0 1px var(--color-border); + vertical-align: middle; +} + +.header__logo-circle > img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + max-width: none; + object-fit: cover; + object-position: center; + display: block; + transform: scale(1.18); + transform-origin: center center; } .header__logo a:hover { @@ -512,8 +540,8 @@ label.search { font-size: 2.2rem; } - .header__logo img { + .header__logo-circle { + width: 40px; height: 40px; - vertical-align: bottom; } } diff --git a/assets/theme/local/og-image.jpg b/assets/theme/local/og-image.jpg new file mode 100644 index 0000000..24df4f8 Binary files /dev/null and b/assets/theme/local/og-image.jpg differ diff --git a/compose.override.yaml b/compose.override.yaml index a4231f5..a3e7f51 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -15,6 +15,11 @@ services: environment: # See https://xdebug.org/docs/all_settings#mode XDEBUG_MODE: "${XDEBUG_MODE:-off}" + ports: + # Defaults avoid crowded 8080/8443; override with HTTP_PORT / HTTPS_PORT in .env + - "127.0.0.1:${HTTP_PORT:-9080}:80/tcp" + - "127.0.0.1:${HTTPS_PORT:-9443}:443/tcp" + - "127.0.0.1:${HTTPS_PORT:-9443}:443/udp" extra_hosts: # Ensure that host.docker.internal is correctly defined on Linux - host.docker.internal:host-gateway @@ -23,6 +28,7 @@ services: ###> doctrine/doctrine-bundle ### database: restart: always + # Optional host access for mysql clients (3307 avoids clashing with a local MySQL on 3306). ports: - - "5432" + - "127.0.0.1:3307:3306" ###< doctrine/doctrine-bundle ### diff --git a/compose.yaml b/compose.yaml index d384791..a918d76 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,25 +6,14 @@ services: dockerfile: Dockerfile environment: APP_ENV: ${APP_ENV:-dev} - SERVER_NAME: ${SERVER_NAME:-localhost}, php:80 - # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM - DATABASE_URL: mysql://${MYSQL_USER:-app}:${MYSQL_PASSWORD:-!ChangeMe!}@database:3306/${MYSQL_DATABASE:-app}?serverVersion=${MYSQL_VERSION:-8.0}&charset=${MYSQL_CHARSET:-utf8mb4} + # Caddy site address: :80 accepts any Host (needed when the app is reached via localhost:HTTP_PORT). + SERVER_NAME: ${SERVER_NAME:-:80} + # Defaults match .env.dist so a first boot without a .env file creates the same DB user as copying .env.dist later. + DATABASE_URL: mysql://${MYSQL_USER:-unfold_user}:${MYSQL_PASSWORD:-password}@database:3306/${MYSQL_DATABASE:-unfold_db}?serverVersion=${MYSQL_VERSION:-8.0}&charset=${MYSQL_CHARSET:-utf8mb4} volumes: - caddy_data:/data - caddy_config:/config - ports: - # HTTP - - target: 80 - published: 80 - protocol: tcp - # HTTPS - - target: 443 - published: 443 - protocol: tcp - # HTTP/3 - - target: 443 - published: 443 - protocol: udp + # Host port publishing: see compose.override.yaml (dev) and compose.prod.yaml (prod). ###> doctrine/doctrine-bundle ### @@ -35,10 +24,10 @@ services: database: image: mysql:${MYSQL_VERSION:-8.0} environment: - MYSQL_DATABASE: ${MYSQL_DATABASE:-app} - MYSQL_USER: ${MYSQL_USER:-app} - MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!} - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-!ChangeRootPassword!} + MYSQL_DATABASE: ${MYSQL_DATABASE:-unfold_db} + MYSQL_USER: ${MYSQL_USER:-unfold_user} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-password} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password} healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 5s diff --git a/config/unfold.yaml b/config/unfold.yaml index 7c1876f..62a1a02 100644 --- a/config/unfold.yaml +++ b/config/unfold.yaml @@ -1,13 +1,21 @@ +# Site identity and theme (see assets/theme/local/ to override default assets). + parameters: - name: 'GitCitadel' - short_name: 'GitCitadel' - description: 'Unfolding nostr magazines and community articles' + name: 'Nostr, Curated Thoughtfully' + short_name: 'Imwald Blog' + description: 'A selection of my own Nostr long-form articles and articles from other authors, selected for the quality of their writing and the depth of their analysis.' + + og_headline: 'Nostr, Curated Thoughtfully' + og_subheading: 'Imwald Blog by Laeserin' + default_relay: 'wss://TheForest.nostr1.com' - theme: 'space' - theme_color: '#000000' - theme_bg_color: '#ffffff' - npub: 'npub1ez09adke4vy8udk3y2skwst8q5chjgqzym9lpq4u58zf96zcl7kqyry2lz' - d_tag: 'newsroom-magazine-by-newsroom' + + theme: 'imwald' + theme_color: '#8c2f1c' + theme_bg_color: '#f1ebe4' + + npub: 'npub1m4ny6hjqzepn4rxknuq94c2gpqzr29ufkkw7ttcxyak7v43n6vvsajc2jl' + d_tag: 'newsroom-magazine-on-imwald-by-laeserin' community_articles: true external_links: - title: "Unfold" diff --git a/scripts/build-og-image.sh b/scripts/build-og-image.sh new file mode 100755 index 0000000..11e323e --- /dev/null +++ b/scripts/build-og-image.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Regenerate assets/theme/local/og-image.jpg (1200×630, standard OG; JPEG for smaller file than PNG24). +# Layout: ~45% left = painting full-bleed (no card frame); ~55% right = warm panel + type. +# Soft gradient seam (no hard divider); headline + subheading + description from config/unfold.yaml. +# Bottom-right: default theme mark (favicon) at reduced opacity. +# Requires: ImageMagick (convert), fold. Run from repository root: +# ./scripts/build-og-image.sh + +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +YAML="${ROOT}/config/unfold.yaml" +LOGO="${ROOT}/assets/laeserin_logo.png" +DEFAULT_MARK="${ROOT}/assets/theme/default/icons/favicon-96x96.png" +OUT="${ROOT}/assets/theme/local/og-image.jpg" +FONT_TITLE="/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" +FONT_SUB="/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" +FONT_BODY="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" + +W=1200 +H=630 +# Safe inset for *type* and corner mark (previews crop edges); painting still bleeds left. +PAD=48 +# Left column width (45%). +LW=$((W * 45 / 100)) +# Text starts after seam blend region. +TEXT_X=618 +# Vertical start for headline (shifted down for balance; sub/body computed from headline lines). +TITLE_Y=108 + +if [[ ! -f "$LOGO" ]]; then + echo "Missing logo: $LOGO" >&2 + exit 1 +fi + +yaml_scalar() { + local key="$1" + grep -E "^[[:space:]]*${key}:" "$YAML" | head -1 | sed -E "s/^[[:space:]]*${key}:[[:space:]]*['\"](.*)['\"].*/\1/" +} + +NAME="$(yaml_scalar name)" +DESC="$(yaml_scalar description)" +HEADLINE="$(yaml_scalar og_headline)" +SUBHEAD="$(yaml_scalar og_subheading)" +[[ -z "$HEADLINE" ]] && HEADLINE="$NAME" +[[ -z "$SUBHEAD" ]] && SUBHEAD="$NAME" + +if [[ -z "$NAME" || -z "$DESC" ]]; then + echo "Could not read name/description from $YAML" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$OUT")" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +mapfile -t _OG_LINES < <(fold -s -w 44 <<<"$DESC") +DESC_MULTILINE=$(printf '%s\n' "${_OG_LINES[@]}") + +mapfile -t _HL_LINES < <(fold -s -w 22 <<<"$HEADLINE") +HEAD_MULTILINE=$(printf '%s\n' "${_HL_LINES[@]}") + +# Right panel: deeper warm linen (reads better on dark-mode surfaces than very pale beige). +CANVAS='#d8cdc0' +convert -size "${W}x${H}" xc:"$CANVAS" "$TMP/canvas.png" + +# Left: full-bleed cover crop; richer reds / saturation, slightly darker mids for depth. +convert "$LOGO" -fuzz 8% -trim +repage \ + -resize "${LW}x${H}^" -gravity center -extent "${LW}x${H}" \ + -gamma 0.96 -modulate 93,138,104 -unsharp 0x0.6+0.52+0.014 \ + "$TMP/left.png" + +convert "$TMP/canvas.png" "$TMP/left.png" -geometry +0+0 -compose over -composite "$TMP/base.png" + +# Full-width wash: transparent at the far left → gentle panel tone on the right. +# Horizontal blur removes a visible “stripe” at the old painting/canvas boundary (LW). +# Text is drawn after this step so it stays crisp on top. +# Lighter wash than before so bodice reds survive; tint anchors toward the darker canvas. +convert -size "${W}x${H}" 'gradient:rgba(216,205,192,0)-rgba(175,158,142,0.26)' "$TMP/seam_raw.png" +# Blur mostly horizontally (rotate trick) so the ramp has no sharp column. +convert "$TMP/seam_raw.png" -rotate 90 -blur 0x42 -rotate -90 "$TMP/seam_blur.png" +convert "$TMP/base.png" "$TMP/seam_blur.png" -compose over -composite "$TMP/blended.png" + +# Stack subheading / body under multi-line headline. +HL_COUNT=${#_HL_LINES[@]} +LINE_H=58 +SUB_Y=$((TITLE_Y + HL_COUNT * LINE_H + 20)) +BODY_Y=$((SUB_Y + 42)) + +# Type colours tuned for the darker panel. +convert "$TMP/blended.png" \ + -font "$FONT_TITLE" -pointsize 52 -fill '#14100e' -annotate +${TEXT_X}+${TITLE_Y} "$HEAD_MULTILINE" \ + -font "$FONT_SUB" -pointsize 27 -fill '#6e2218' -annotate +${TEXT_X}+${SUB_Y} "$SUBHEAD" \ + -font "$FONT_BODY" -pointsize 23 -fill '#2a221c' -interline-spacing 6 \ + -annotate +${TEXT_X}+${BODY_Y} "$DESC_MULTILINE" \ + PNG24:"$TMP/with_type.png" + +# Default newsroom mark — tinted, low opacity so it belongs in the corner. +if [[ -f "$DEFAULT_MARK" ]]; then + # Circular mark (soft vs a square stamp on the OG card). + convert "$DEFAULT_MARK" -resize 88x88 \ + \( -size 88x88 xc:none -fill white -draw "circle 44,44 44,0" \) \ + -compose DstIn -composite -alpha set \ + -modulate 105,85,95 \ + -channel A -evaluate multiply 0.48 +channel \ + "$TMP/mark.png" + convert "$TMP/with_type.png" \( "$TMP/mark.png" \) -gravity SouthEast -geometry +${PAD}+${PAD} -compose over -composite "$TMP/marked.png" +else + cp "$TMP/with_type.png" "$TMP/marked.png" +fi + +# Global: a touch darker + more saturated so the whole card holds up on dark UI chrome. +# JPEG (not PNG24): social previews are fine with lossy compression; much smaller on-disk. +convert "$TMP/marked.png" -modulate 96,118,102 -unsharp 0x0.55+0.48+0.012 \ + -quality 86 -sampling-factor 4:2:0 -strip "$OUT" + +rm -f "${ROOT}/assets/theme/local/og-image.png" + +echo "Wrote $OUT ($(wc -c <"$OUT") bytes)" diff --git a/templates/base.html.twig b/templates/base.html.twig index 1057131..e422c52 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -6,7 +6,7 @@