Browse Source

branding commit

imwald
Silberengel 1 week ago
parent
commit
6dc766a6ec
  1. 6
      .env.dist
  2. BIN
      assets/laeserin.png
  3. BIN
      assets/laeserin_icon.png
  4. BIN
      assets/laeserin_logo.png
  5. 36
      assets/styles/app.css
  6. BIN
      assets/theme/local/og-image.jpg
  7. 8
      compose.override.yaml
  8. 29
      compose.yaml
  9. 24
      config/unfold.yaml
  10. 121
      scripts/build-og-image.sh
  11. 4
      templates/base.html.twig
  12. 2
      templates/components/Header.html.twig
  13. 4
      templates/pages/article.html.twig
  14. 1
      templates/pages/category.html.twig

6
.env.dist

@ -19,7 +19,11 @@ APP_ENV=dev
APP_SECRET=9e287f1ad737386dde46d51e80487236 APP_SECRET=9e287f1ad737386dde46d51e80487236
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
###> docker ### ###> 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_DATABASE=unfold_db
MYSQL_VERSION=8.0 MYSQL_VERSION=8.0
MYSQL_CHARSET=utf8mb4 MYSQL_CHARSET=utf8mb4

BIN
assets/laeserin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
assets/laeserin_icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
assets/laeserin_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

36
assets/styles/app.css

@ -29,6 +29,10 @@ h1.brand {
font-family: var(--brand-font), serif; font-family: var(--brand-font), serif;
font-size: 3.6rem; font-size: 3.6rem;
color: var(--brand-color); 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 { .truncate {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; /* limit to 3 lines */ -webkit-line-clamp: 3; /* limit to 3 lines */
line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -297,9 +302,32 @@ div:nth-child(odd) .featured-list {
font-weight: normal; 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; 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 { .header__logo a:hover {
@ -512,8 +540,8 @@ label.search {
font-size: 2.2rem; font-size: 2.2rem;
} }
.header__logo img { .header__logo-circle {
width: 40px;
height: 40px; height: 40px;
vertical-align: bottom;
} }
} }

BIN
assets/theme/local/og-image.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

8
compose.override.yaml

@ -15,6 +15,11 @@ services:
environment: environment:
# See https://xdebug.org/docs/all_settings#mode # See https://xdebug.org/docs/all_settings#mode
XDEBUG_MODE: "${XDEBUG_MODE:-off}" 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: extra_hosts:
# Ensure that host.docker.internal is correctly defined on Linux # Ensure that host.docker.internal is correctly defined on Linux
- host.docker.internal:host-gateway - host.docker.internal:host-gateway
@ -23,6 +28,7 @@ services:
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
database: database:
restart: always restart: always
# Optional host access for mysql clients (3307 avoids clashing with a local MySQL on 3306).
ports: ports:
- "5432" - "127.0.0.1:3307:3306"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###

29
compose.yaml

@ -6,25 +6,14 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
environment: environment:
APP_ENV: ${APP_ENV:-dev} APP_ENV: ${APP_ENV:-dev}
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80 # Caddy site address: :80 accepts any Host (needed when the app is reached via localhost:HTTP_PORT).
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM SERVER_NAME: ${SERVER_NAME:-:80}
DATABASE_URL: mysql://${MYSQL_USER:-app}:${MYSQL_PASSWORD:-!ChangeMe!}@database:3306/${MYSQL_DATABASE:-app}?serverVersion=${MYSQL_VERSION:-8.0}&charset=${MYSQL_CHARSET:-utf8mb4} # 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: volumes:
- caddy_data:/data - caddy_data:/data
- caddy_config:/config - caddy_config:/config
ports: # Host port publishing: see compose.override.yaml (dev) and compose.prod.yaml (prod).
# HTTP
- target: 80
published: 80
protocol: tcp
# HTTPS
- target: 443
published: 443
protocol: tcp
# HTTP/3
- target: 443
published: 443
protocol: udp
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
@ -35,10 +24,10 @@ services:
database: database:
image: mysql:${MYSQL_VERSION:-8.0} image: mysql:${MYSQL_VERSION:-8.0}
environment: environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-app} MYSQL_DATABASE: ${MYSQL_DATABASE:-unfold_db}
MYSQL_USER: ${MYSQL_USER:-app} MYSQL_USER: ${MYSQL_USER:-unfold_user}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!} MYSQL_PASSWORD: ${MYSQL_PASSWORD:-password}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-!ChangeRootPassword!} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password}
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 5s timeout: 5s

24
config/unfold.yaml

@ -1,13 +1,21 @@
# Site identity and theme (see assets/theme/local/ to override default assets).
parameters: parameters:
name: 'GitCitadel' name: 'Nostr, Curated Thoughtfully'
short_name: 'GitCitadel' short_name: 'Imwald Blog'
description: 'Unfolding nostr magazines and community articles' 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' default_relay: 'wss://TheForest.nostr1.com'
theme: 'space'
theme_color: '#000000' theme: 'imwald'
theme_bg_color: '#ffffff' theme_color: '#8c2f1c'
npub: 'npub1ez09adke4vy8udk3y2skwst8q5chjgqzym9lpq4u58zf96zcl7kqyry2lz' theme_bg_color: '#f1ebe4'
d_tag: 'newsroom-magazine-by-newsroom'
npub: 'npub1m4ny6hjqzepn4rxknuq94c2gpqzr29ufkkw7ttcxyak7v43n6vvsajc2jl'
d_tag: 'newsroom-magazine-on-imwald-by-laeserin'
community_articles: true community_articles: true
external_links: external_links:
- title: "Unfold" - title: "Unfold"

121
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)"

4
templates/base.html.twig

@ -6,7 +6,7 @@
<title>{% block title %}{{ website_name }}{% endblock %}</title> <title>{% block title %}{{ website_name }}{% endblock %}</title>
<meta name="description" content="{{ website_description }}"> <meta name="description" content="{{ website_description }}">
<link rel="icon" type="image/png" href="{{ asset('icons/favicon-96x96.png') }}" sizes="96x96" /> <link rel="icon" type="image/png" href="{{ asset('icons/favicon-96x96.png') }}" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="{{ asset('icons/favicon.svg') }}" /> <link rel="icon" type="image/x-icon" href="{{ asset('icons/favicon.ico') }}" />
<link rel="shortcut icon" href="{{ asset('icons/favicon.ico') }}" /> <link rel="shortcut icon" href="{{ asset('icons/favicon.ico') }}" />
<link rel="apple-touch-icon" href="{{ asset('icons/apple-touch-icon.png') }}"> <link rel="apple-touch-icon" href="{{ asset('icons/apple-touch-icon.png') }}">
<meta name="apple-mobile-web-app-title" content="{{ website_short_name }}" /> <meta name="apple-mobile-web-app-title" content="{{ website_short_name }}" />
@ -14,7 +14,7 @@
<link rel="manifest" href="{{ path('pwa_manifest') }}"> <link rel="manifest" href="{{ path('pwa_manifest') }}">
{% block ogtags %} {% block ogtags %}
<meta property="og:image" content="{{ asset('icons/favicon.ico') }}" /> <meta property="og:image" content="{{ absolute_url(asset('og-image.jpg')) }}" />
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %} {% block importmap %}{{ importmap('app') }}{% endblock %}

2
templates/components/Header.html.twig

@ -2,7 +2,9 @@
<div class="header__logo"> <div class="header__logo">
<a href="/" class="header__brand"> <a href="/" class="header__brand">
<h1 class="brand"> <h1 class="brand">
<span class="header__logo-circle">
<img src="{{ asset('icons/favicon-96x96.png') }}" alt="{{ website_short_name }}" class="logo" /> <img src="{{ asset('icons/favicon-96x96.png') }}" alt="{{ website_short_name }}" class="logo" />
</span>
{{ website_name }} {{ website_name }}
</h1> </h1>
</a> </a>

4
templates/pages/article.html.twig

@ -6,9 +6,11 @@
<meta property="og:url" content="{{ app.request.uri }}"> <meta property="og:url" content="{{ app.request.uri }}">
{% if article.image %} {% if article.image %}
<meta property="og:image" content="{{ article.image }}"> <meta property="og:image" content="{{ article.image }}">
{% else %}
<meta property="og:image" content="{{ absolute_url(asset('og-image.jpg')) }}">
{% endif %} {% endif %}
<meta property="og:description" content="{{ article.summary|striptags|u.truncate(159,'…')|e }}"> <meta property="og:description" content="{{ article.summary|striptags|u.truncate(159,'…')|e }}">
<meta property="og:site_name" content="Newsroom"> <meta property="og:site_name" content="{{ website_name }}">
{% endblock %} {% endblock %}
{% block body %} {% block body %}

1
templates/pages/category.html.twig

@ -5,6 +5,7 @@
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="{{ app.request.uri }}"> <meta property="og:url" content="{{ app.request.uri }}">
<meta property="og:description" content="{{ category.summary }}"> <meta property="og:description" content="{{ category.summary }}">
<meta property="og:image" content="{{ absolute_url(asset('og-image.jpg')) }}">
<meta property="og:site_name" content="{{ website_name }}"> <meta property="og:site_name" content="{{ website_name }}">
{% endblock %} {% endblock %}

Loading…
Cancel
Save