Liten statisk blogg med mobilvennlig Markdown-editor, bildeopplasting og innhold i git. Én hovedbruker, valgfrie gjester.
  • Python 77.7%
  • HTML 10.4%
  • JavaScript 4.9%
  • CSS 4.7%
  • Shell 1.6%
  • Other 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ole-Morten Duesund 9b6fc756b2 Rett utdatert beskrivelse av hva sqlite-databasen inneholder
db.py og README sa at bare brukere og passord-hasher ligger i databasen,
men passnøkler, OAuth-tokens og opplastingstokens ligger der også.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BuSvQFJ3LQqJLEiWXHTueN
2026-10-12 19:07:16 +02:00
soapbox Rett utdatert beskrivelse av hva sqlite-databasen inneholder 2026-10-12 19:07:16 +02:00
tests MCP: add_image tar imot SVG og lagrer den som PNG 2026-10-12 19:04:21 +02:00
.env.example MCP: opplastings-URL med engangstoken (request_image_upload) 2026-10-12 19:01:07 +02:00
.gitignore run.sh: automatisk backup med rotasjon, og --restart=always 2026-08-26 14:41:16 +02:00
ARCHITECTURE.md Legg til ARCHITECTURE.md 2026-10-12 19:06:03 +02:00
Caddyfile.example MCP-server med OAuth, så Claude kan skrive og publisere innlegg 2026-10-12 14:56:32 +02:00
compose.yaml Slå sammen build.sh og run.sh til deploy.sh 2026-10-12 15:09:58 +02:00
Containerfile MCP: add_image tar imot SVG og lagrer den som PNG 2026-10-12 19:04:21 +02:00
deploy.sh Slå sammen build.sh og run.sh til deploy.sh 2026-10-12 15:09:58 +02:00
LICENSE Lisensier under AGPL-3.0-or-later og lenk til kildekoden i footeren 2026-08-26 13:13:25 +02:00
pyproject.toml MCP: add_image tar imot SVG og lagrer den som PNG 2026-10-12 19:04:21 +02:00
README.md Rett utdatert beskrivelse av hva sqlite-databasen inneholder 2026-10-12 19:07:16 +02:00
uv.lock MCP: add_image tar imot SVG og lagrer den som PNG 2026-10-12 19:04:21 +02:00

Soapbox

En liten blogg: statiske sider, mobilvennlig Markdown-editor med bildeopplasting og live forhåndsvisning, innhold versjonert i git. Én hovedbruker («soapbox») pluss valgfrie gjester.

Hvordan det henger sammen

data/
├── content/            git-repo – alt innhold, kan klones/flyttes fritt
│   ├── site.toml       tittel, beskrivelse, språk
│   ├── posts/<slug>/index.md + bilder        → /<slug>/
│   └── guests/<bruker>/<slug>/index.md       → /<bruker>/<slug>/
├── public/             generert statisk side (bygges automatisk ved lagring)
├── soapbox.sqlite      brukere, passnøkler og tokens – det eneste som ikke ligger i git
└── secret_key          cookie-signering (genereres automatisk)
  • Slug lages av tittelen (Grønt er skjønt → grønt-er-skjønt). Den følger tittelen så lenge innlegget er utkast, og fryses ved publisering så lenker aldri brekker.
  • Alle interne lenker er relative. Malene regner ut ../ ut fra sidens dybde, og absolutte lenker du skriver i Markdown (/annet-innlegg/) skrives om. Derfor fungerer samme public/ både på blog.domene.no/ og annet-domene.com/blog/ uten rebuild.
  • Bilder ligger i samme mappe som innlegget og refereres som ![](bilde.jpg). Ved opplasting skaleres de ned (maks 2000 px) og EXIF fjernes (ingen GPS-posisjon publiseres).
  • Hver lagring er en git-commit med brukeren som forfatter. Hvert innlegg har en «Historikk»-seksjon i editoren med diff per endring; hele loggen ligger under Innstillinger (eller git -C data/content log).
  • Gjester ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester.
  • Med påholden penn: en gjest kan under Profil tillate at hovedbrukeren skriver innlegg i gjestens navn. Hovedbrukeren får da «Skriv for gjest» på innleggslisten. Innlegget havner under gjestens /<bruker>/, frontmatter får penholder: "<hovedbruker>", og siden viser «med påholden penn» ved datoen og navnet i signaturen. Git-committen står på den som faktisk skrev. Trekkes samtykket tilbake, kan det ikke opprettes nye slike innlegg, men eksisterende beholder merkingen.
  • Passnøkler: under Innlogging i admin kan hver bruker legge til passnøkler (fingeravtrykk, ansikt eller PIN på telefon/PC, eller en sikkerhetsnøkkel) og deretter logge inn uten brukernavn og passord. Nøklene er bundet til domenet i SOAPBOX_SITE_URL, så admin må åpnes via den adressen – ikke via IP eller et annet vertsnavn. Passord virker fortsatt ved siden av.
  • Footeren viser «Dashbord» i stedet for «Logg inn» når du er innlogget. Sidene er statiske, så theme/session.js leser en ikke-hemmelig cookie (soapbox_in) som settes ved innlogging. Egne temaer med egen base.html må ta med skriptet og data-admin-link på lenken for å få det.
  • Tagger settes kommaseparert i editoren. /tag/<tagg>/ lister alle innlegg med taggen (også gjesters), /tag/ alle tagger. Tagger får slug på samme måte som titler.
  • Brukersider: /brukere/ lister hovedbruker øverst og gjester alfabetisk; /brukere/<navn>/ viser visningsnavn, beskrivelse (Markdown, settes under Profil), e-post og innlegg.
  • Signatur: innlegg avsluttes med «– Visningsnavn», evt. med e-post som mailto-lenke. Begge settes under Profil i admin og slås opp ved bygging, så en endring gjelder alle innlegg.

Se ARCHITECTURE.md for moduler, dataflyt og sikkerhetsgrenser.

Skrive med Claude (MCP)

Bloggen har en MCP-server på <SOAPBOX_SITE_URL>/mcp, så Claude kan skrive, endre og publisere innlegg direkte. Den er av som standard; hovedbrukeren slår den på under Innstillinger → Tillat Claude (MCP). Når den er av, svarer alle MCP- og OAuth-adresser 404, og avslåing kobler fra alle apper. Legg den til i claude.ai eller Claude Desktop under Innstillinger → Connectors → Legg til egen connector med adressen, f.eks. https://naiv.no/bloggen/mcp. Claude Code: claude mcp add --transport http soapbox https://naiv.no/bloggen/mcp.

  • Innlogging skjer med OAuth: Claude registrerer seg selv, du sendes til admin, logger inn som vanlig (passord eller passnøkkel) og godkjenner. Claude får da samme rettigheter som deg – hovedbrukeren kan redigere alt, gjester bare egne innlegg.
  • Verktøy: list_posts, get_post, create_post, update_post, add_image, request_image_upload og delete_post. Nye innlegg blir utkast med mindre Claude ber om publisering; slug- og datoreglene er de samme som i editoren, og hver endring blir en git-commit med deg som forfatter.
  • Bilder Claude har lokalt lastes helst opp via request_image_upload: verktøyet gir en engangs-URL (<SOAPBOX_SITE_URL>/api/upload/<token>) som Claude laster opp til med curl -T <fil> <url>, så bildedataene ikke går gjennom MCP-kallet. Tokenet er eneste autentisering, gjelder ett innlegg, kan brukes én gang og utløper etter SOAPBOX_UPLOAD_TOKEN_TTL sekunder. ?alt= overstyrer alt-teksten. Alternativt kan add_image hente fra en https-URL (serveren henter selv, og nekter interne adresser som localhost, private nett og sky-metadata) eller ta imot base64. Bildene skaleres ned og EXIF fjernes som ved vanlig opplasting.
  • Illustrasjoner Claude lager selv kan sendes som SVG-tekst til add_image (svg, valgfri width). Serveren rasteriserer med resvg og lagrer bare PNG-en; SVG-en serveres aldri. SVG med skript, foreignObject, on*-attributter, DTD/entiteter eller eksterne referanser avvises. Tekst bruker systemfontene (DejaVu i containeren).
  • Med påholden penn: create_post med on_behalf: "<gjest>" virker for hovedbrukeren når gjesten har tillatt det under Profil, akkurat som «Skriv for gjest» i admin.
  • Koble fra under Innlogging i admin. Bytter du passord, kobles alle apper fra.
  • Bare SHA-256 av tokens lagres i soapbox.sqlite. Access-tokens varer en time, refresh-tokens 30 dager og roteres ved bruk.
  • Under sub-path finner Claude OAuth-oppsettet under /bloggen/ selv, men eldre klienter leter på domeneroten (/.well-known/oauth-authorization-server/bloggen o.l.). Rut disse stiene til appen, se Caddyfile.example.

Kjøre lokalt

uv sync
SOAPBOX_DATA_DIR=./data uv run soapbox create-user olemd --role owner
SOAPBOX_DATA_DIR=./data uv run soapbox serve --debug
# → http://localhost:8080/admin/

Tester: uv run pytest. Lint: uv run ruff check . && uv run ruff format .

Konfigurasjon (miljøvariabler)

Se .env.example for en kommentert mal. Alt har standardverdier; kun SOAPBOX_SITE_URL og hovedbrukeren bør settes.

Variabel Standard Beskrivelse
SOAPBOX_DATA_DIR ./data (/data i container) Alt persistent
SOAPBOX_BASE_PATH stien i SOAPBOX_SITE_URL Overstyring, kun hvis proxyen stripper prefikset
SOAPBOX_SITE_URL http://localhost:8080 Absolutt URL til rot inkl. sub-path; brukes i Open Graph, feed, sitemap og til å utlede base-path
SOAPBOX_THEME green Mappe under soapbox/themes/
SOAPBOX_MAX_IMAGE_PX 2000 Lengste side på opplastede bilder
SOAPBOX_UPLOAD_TOKEN_TTL 600 Sekunder en opplastings-URL fra request_image_upload er gyldig
SOAPBOX_UPLOAD_MAX_BYTES 20971520 Maks størrelse på fil lastet opp til en slik URL
SOAPBOX_SECRET_KEY autogenerert Overstyr hvis du vil ha nøkkelen utenfor volumet
SOAPBOX_OWNER_USER / _PASSWORD – Oppretter hovedbruker ved første start

Container og Caddy

cp .env.example .env        # sett SOAPBOX_SITE_URL og hovedbruker
./deploy.sh                 # bygger localhost/soapbox:latest og starter containeren med .env
./deploy.sh --build-only    # bare bygg (med build-metadata)
./deploy.sh --run-only      # bare start/erstatt containeren, lytter på SOAPBOX_BIND:SOAPBOX_PORT
# eller: podman compose up -d   (se ./deploy.sh --help for alle valg)

Ny versjon: git pull && ./deploy.sh. Data ligger i podman-volumet soapbox-data (podman volume inspect soapbox-data viser stien).

deploy.sh tar automatisk backup av volumet før hver oppstart til SOAPBOX_BACKUP_DIR (standard ./backups/soapbox-data-<tidsstempel>.tar.gz) og beholder de SOAPBOX_BACKUP_KEEP nyeste (standard 5). Gjenopprett med podman volume import soapbox-data <fil>.

Caddy er ren reverse proxy, se Caddyfile.example. Appen lytter som standard på alle adresser (IPv4 og IPv6), så Caddy kan stå på en annen maskin og nå den over Tailscale/MagicDNS. Sett SOAPBOX_BIND=127.0.0.1 for å begrense når Caddy kjører lokalt. Ved sub-path skal Caddy ikke strippe prefikset – appen monteres selv under stien fra SOAPBOX_SITE_URL.

Flytte bloggen (nytt domene og/eller sub-path)

Ingen innhold trenger endring – alle lenker er relative. Velg én av to måter:

A. Kopier hele data/-volumet. Tar med innhold, brukere og cookie-nøkkel. Ingenting må opprettes på nytt.

podman volume export soapbox-data | ssh ny-server 'podman volume import soapbox-data -'

B. Klon bare innholdsrepoet. Brukere ligger i sqlite, ikke i git, så de må opprettes på nytt (hovedbruker via SOAPBOX_OWNER_USER/_PASSWORD eller soapbox create-user, gjester i admin med samme brukernavn som før – brukernavnet er URL-prefikset /gjest/).

git clone gammel-server:/sti/til/data/content ./data/content

Deretter, uansett metode: sett SOAPBOX_SITE_URL til den nye adressen (sub-path utledes derfra) og start. Siden bygges på nytt ved oppstart.

Tema

Aktivt tema velges under Innstillinger i admin og lagres i site.toml (følger med i git). SOAPBOX_THEME er bare standardverdien før noe er valgt.

Lag et eget tema uten å bygge containeren på nytt: legg det i innholdsrepoet, så følger det med i git, backup og ved flytting.

mkdir -p data/content/themes/skog/static
cp soapbox/themes/green/static/style.css data/content/themes/skog/static/   # rediger fargene

Et tema kan være delvis: bare filene som finnes overstyrer det innebygde green-temaet. Vil du endre malene, legg templates/base.html, index.html eller post.html i temamappen. Malene får site, posts/post, html, source_url og rel(sti) (relativ lenke fra gjeldende side), samt filtrene date, excerpt(n) og tagslug. Bruk asset('theme/fil.css') for temafiler: den gir en relativ URL med innholdshash (?v=…) for cache-busting. static/ havner under /theme/.

Lisens

AGPL-3.0-or-later, se LICENSE. Bloggen lenker til kildekoden i footeren, slik lisensen krever for nettverkstjenester. Endrer du koden og hoster den, må lenken peke til din versjon (SOURCE_URL i soapbox/build.py).

Veien videre (ikke i denne omgangen)

  • Poste lenker til fediverset/Bluesky og vise svar som kommentarer.
  • Push av innholdsrepoet til en ekstern git-remote.