From 13ccbd5b9bbc9ddb013ad1bdbb68099a0eb4f27d Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Sun, 12 Apr 2026 09:14:15 +0000 Subject: [PATCH] Add .env support for configurable Name and Slogan in preview cards NAME and SLOGAN are read from .env via dotenv and injected into the LinkedIn preview card template at startup. Avatar initials are auto- generated from the first letters of NAME. Works identically with npm start and docker compose (via env_file). Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 2 ++ CLAUDE.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 1 + package-lock.json | 13 +++++++++++++ package.json | 1 + public/index.html | 4 ++-- server.js | 14 +++++++++++++- 7 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 .env.example create mode 100644 CLAUDE.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2a2505d --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NAME=Max Mustermann +SLOGAN=Senior Developer · Tech Enthusiast diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7612528 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,47 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +A self-hosted LinkedIn Post Formatter that converts text with formatting markers (`**bold**`, `_italic_`) into Unicode Mathematical Alphanumeric Symbols. LinkedIn only accepts plain Unicode text (no HTML/Markdown), so the tool converts text into Unicode variants that *appear* formatted in the LinkedIn feed. + +## Running the Project + +```bash +npm start # Start server on port 3000 +PORT=8080 node server.js # Custom port +docker compose up -d # Docker deployment +``` + +There are no build, test, or lint steps. + +## Architecture + +The project is intentionally minimal: + +- **`server.js`** — 16-line Express server that only serves static files from `public/` and supports a `PORT` env var. +- **`public/index.html`** — The entire application: HTML, CSS, and JavaScript all inlined in one file (~289 lines). No frameworks, no bundler, no external dependencies. + +All formatting logic lives in `public/index.html` as vanilla JavaScript: + +- `applyInline(text, variant)` — Processes `**text**` (bold) and `_text_` (italic) markers within a variant pass +- `mapAll(text, map)` — Applies Unicode character mapping char-by-char +- `liParagraph(text)` — Inserts zero-width spaces (`\u200b`) between double newlines to preserve paragraph breaks when pasting into LinkedIn (which strips trailing blank lines) +- Six output variants: Sans (standard), Bold Sans, Italic Sans, Bold Italic Sans, Monospace, Plain + +The Unicode character maps are JavaScript objects mapping A–Z, a–z, 0–9 to their Unicode Mathematical Alphanumeric equivalents (e.g., bold sans, italic sans, monospace). + +## Key Implementation Details + +**Zero-width space trick:** LinkedIn strips trailing blank lines on paste. The app inserts `\u200b` between `\n\n` sequences to preserve paragraph structure. + +**Character limit:** LinkedIn max is 3000 characters. The counter uses color-coded feedback (green → amber → red) with `toLocaleString('de-DE')` formatting. + +**Clipboard fallback:** Uses `navigator.clipboard.writeText()` with a `document.execCommand('copy')` fallback for older browsers. + +**Keyboard shortcuts:** Ctrl/Cmd+B (bold), Ctrl/Cmd+I (italic), Ctrl/Cmd+Enter (generate preview). + +## Language + +The UI and all user-facing text is in **German**. Keep new UI additions in German. diff --git a/docker-compose.yml b/docker-compose.yml index 9c3e834..a429243 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,3 +5,4 @@ services: ports: - "3000:3000" restart: unless-stopped + env_file: .env diff --git a/package-lock.json b/package-lock.json index 283a2b0..8237c54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "linkedin-formatter", "version": "1.1.0", "dependencies": { + "dotenv": "^17.4.1", "express": "^4.18.2" } }, @@ -156,6 +157,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index cb86b72..933f5b3 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "start": "node server.js" }, "dependencies": { + "dotenv": "^17.4.1", "express": "^4.18.2" } } diff --git a/public/index.html b/public/index.html index 311c0db..4e3db0a 100644 --- a/public/index.html +++ b/public/index.html @@ -189,8 +189,8 @@ function generate(){
${v.label}
-
JH
-
Joachim Hummel
Senior IT Consultant · Automatisierung
+
{{INITIALS}}
+
{{NAME}}
{{SLOGAN}}
${esc(v.text)}
diff --git a/server.js b/server.js index 75504b6..736416d 100644 --- a/server.js +++ b/server.js @@ -1,14 +1,26 @@ 'use strict'; +require('dotenv').config(); const express = require('express'); const path = require('path'); +const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 3000; +const NAME = process.env.NAME || 'Name'; +const SLOGAN = process.env.SLOGAN || ''; +const INITIALS = NAME.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); + app.use(express.static(path.join(__dirname, 'public'))); app.get('/', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'index.html')); + let html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8'); + html = html + .replace('{{INITIALS}}', INITIALS) + .replace('{{NAME}}', NAME) + .replace('{{SLOGAN}}', SLOGAN); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.send(html); }); app.listen(PORT, () => {