The blogroll index.js is an ES module so require() is not defined.
Replace `const { Router } = require("express")` with `express.Router()`
which is already in scope from the module's own top-level import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Indieweb/kit Blog Server
Admin login
- The IndieKit admin uses root auth/session paths (for example:
/session/login,/auth,/auth/new-password). - Login uses
PASSWORD_SECRET(bcrypt hash), notINDIEKIT_PASSWORD. - If no
PASSWORD_SECRETexists yet, open/auth/new-passwordonce to generate it. - If login is blocked because
PASSWORD_SECRETis missing/invalid, setINDIEKIT_ALLOW_PASSWORD_SETUP=1temporarily, restart, generate a new hash via/auth/new-password, setPASSWORD_SECRETto that hash, then removeINDIEKIT_ALLOW_PASSWORD_SETUP. - If login appears passwordless, first check for an existing authenticated session cookie. Use
/session/logoutto force a fresh login challenge. - Upstream IndieKit auto-authenticates in dev mode (
NODE_ENV=development). This repository patches that behavior so dev auto-auth only works whenINDIEKIT_ALLOW_DEV_AUTH=1is explicitly set. - Production startup now fails closed when auth/session settings are unsafe (
NODE_ENVnotproduction,INDIEKIT_ALLOW_DEV_AUTH=1, weakSECRET, missing/invalidPASSWORD_SECRET, or empty-password hash). - Post management UI should use
/posts(@indiekit/endpoint-posts.mountPath). - Do not set post-management
mountPathto frontend routes like/blog, otherwise backend publishing can be shadowed by the public site.
Backend endpoints
- Configured endpoint mount paths:
- Posts management:
/posts - Files:
/files - Webmentions moderation + API:
/webmentions - Webmentions proxy API:
/webmentions-api - Webmention sender + API:
/webmention-sender - Homepage builder UI + API:
/homepage - Conversations + API:
/conversations - GitHub activity + API:
/github - Funkwhale activity + API:
/funkwhale - Last.fm activity + API:
/lastfmapi - Podroll dashboard + API:
/podrollapi - ActivityPub federation + admin reader:
/activitypub - ActivityPub discovery:
/.well-known/webfinger,/nodeinfo/2.1
MongoDB
- Preferred: set
MONGO_USERNAMEandMONGO_PASSWORDexplicitly; config builds the URL fromMONGO_USERNAME,MONGO_PASSWORD,MONGO_HOST,MONGO_PORT,MONGO_DATABASE,MONGO_AUTH_SOURCE. - You can still use a full
MONGO_URL(example:mongodb://user:pass@host:27017/indiekit?authSource=admin). - If both
MONGO_URLandMONGO_USERNAME/MONGO_PASSWORDare set, decomposed credentials take precedence by default to avoid stale URL mismatches. SetMONGO_PREFER_URL=1to forceMONGO_URLprecedence. - Startup scripts now fail fast when
MONGO_URLis absent andMONGO_USERNAMEis missing, to avoid silent auth mismatches. - Startup now runs
scripts/preflight-mongo-connection.mjsbefore boot. Preflight is strict by default and aborts start on Mongo auth/connect failures; setREQUIRE_MONGO=0to bypass strict mode intentionally. - For
MongoServerError: Authentication failed, first verifyMONGO_PASSWORD, then tryMONGO_AUTH_SOURCE=admin.
Content paths
- This setup writes post files to the content repo
blogundercontent/. - Photo upload binaries are written to
images/{filename}and published at${PUBLICATION_URL}/images/{filename}. - Current paths in
publication.postTypesare: content/articles/{slug}.mdcontent/notes/{slug}.mdcontent/bookmarks/{slug}.mdcontent/likes/{slug}.mdcontent/photos/{slug}.mdcontent/replies/{slug}.mdcontent/pages/{slug}.md- If these paths do not match the content repo structure, edit/delete actions can fail with GitHub
Not Found. - Reposts are configured as a dedicated post type (
repost) and stored atcontent/reposts/{slug}.md.
Post URLs
- Current post URLs in
publication.postTypesare: https://blog.giersig.eu/articles/{slug}/https://blog.giersig.eu/notes/{slug}/https://blog.giersig.eu/bookmarks/{slug}/https://blog.giersig.eu/likes/{slug}/https://blog.giersig.eu/photos/{slug}/https://blog.giersig.eu/replies/{slug}/https://blog.giersig.eu/{slug}/(page post type)
GitHub tokens
- Recommended for two-repo setups:
GH_CONTENT_TOKEN: token for content repo (blog), used by@indiekit/store-github.GH_ACTIVITY_TOKEN: token for GitHub dashboard/activity endpoint, used by@rmdes/indiekit-endpoint-github.GITHUB_USERNAME: GitHub user/owner name.- Backward compatibility: if
GH_CONTENT_TOKENorGH_ACTIVITY_TOKENare not set, config falls back toGITHUB_TOKEN.
Listening tokens
- Funkwhale endpoint requirements:
FUNKWHALE_INSTANCE(for examplehttps://your-funkwhale.example, root server URL only)FUNKWHALE_USERNAMEFUNKWHALE_TOKEN(read API token)- Last.fm endpoint requirements:
LASTFM_API_KEYLASTFM_USERNAME- Listening endpoint plugins target Node.js 20+; older runtimes can produce inconsistent fetch/JSON behavior.
- If
FUNKWHALE_INSTANCEpoints to a host that does not expose Funkwhale's API routes, API responses now degrade to empty data instead of repeated 500 errors. - If these variables are missing, the endpoints still exist but return empty activity until credentials are configured.
Podroll endpoint
- Podroll endpoint is enabled via
@rmdes/indiekit-endpoint-podrolland mounted at/podrollapiby default. - Optional environment variables:
PODROLL_MOUNT_PATH(default/podrollapi)PODROLL_EPISODES_URL(FreshRSS greader endpoint URL used for episode sync)PODROLL_OPML_URL(FreshRSS OPML export URL used for podcast source sync)- If
PODROLL_EPISODES_URLandPODROLL_OPML_URLare not set, the endpoint still loads and can be configured from its admin dashboard.
Webmention sender
- Webmention sender endpoint is enabled via
@rmdes/indiekit-endpoint-webmention-senderand mounted at/webmention-senderby default. - Optional environment variables:
WEBMENTION_SENDER_MOUNT_PATH(default/webmention-sender)WEBMENTION_SENDER_TIMEOUT(default10000, endpoint discovery timeout in milliseconds)WEBMENTION_SENDER_USER_AGENT(default${SITE_NAME} Webmention Sender)- Startup polling loop variables (used by
start.example.sh): WEBMENTION_SENDER_AUTO_POLL(default1, set0to disable)WEBMENTION_SENDER_POLL_INTERVAL(default300, seconds)WEBMENTION_SENDER_HOST(default127.0.0.1)WEBMENTION_SENDER_PORT(default${PORT}or3000)WEBMENTION_SENDER_ORIGIN(optional JWTmeclaim override, defaultsPUBLICATION_URL->SITE_URL)WEBMENTION_SENDER_ENDPOINT(optional full URL override)POST /webmention-senderrequires authentication (updatescope) and sends pending webmentions for unpublished targets.
Webmentions proxy
- Webmentions proxy endpoint is enabled via
@rmdes/indiekit-endpoint-webmentions-proxyand mounted at/webmentions-apiby default. - Optional environment variables:
WEBMENTIONS_PROXY_MOUNT_PATH(default/webmentions-api)WEBMENTIONS_PROXY_CACHE_TTL(default60, cache TTL in seconds)- Uses existing
WEBMENTION_IO_TOKENandWEBMENTION_IO_DOMAINconfiguration for upstream webmention.io requests. - Public JSON API route:
GET /webmentions-api/api/mentions(supportspage,per-page,target,wm-propertyquery parameters).
ActivityPub
- ActivityPub federation is enabled via
@rmdes/indiekit-endpoint-activitypub. - Actor handle resolution order is:
AP_HANDLE, thenACTIVITYPUB_HANDLE, thenGITHUB_USERNAME, then publication hostname first label. - Actor profile seed values come from
AUTHOR_NAME,AUTHOR_BIO,AUTHOR_AVATAR, andSITE_DESCRIPTION. AUTHOR_AVATARcan be absolute (https://...) or slash-relative (/images/avatar.jpg); startup normalizes it to an absolute URL.- Optional ActivityPub variables:
AP_ALSO_KNOWN_AS(Mastodon migration alias URL)AP_LOG_LEVEL(debug|info|warning|error|fatal, defaultinfo)AP_DEBUG(1ortrueenables debug dashboard)AP_DEBUG_PASSWORD(required when debug dashboard is enabled)REDIS_URL(recommended for production delivery queue durability)- Startup preflight
scripts/preflight-activitypub-rsa-key.mjsensuresap_keyscontains a usable RSA key pair (publicKeyPem+privateKeyPem) so outgoing inbox deliveries are HTTP-signed and not rejected withRequest not signed. - Startup preflight
scripts/preflight-activitypub-profile-urls.mjsnormalizes existing ActivityPub profile URL fields in MongoDB (url,icon,image,alsoKnownAs) so WebFinger/actor responses do not fail on invalid URL values. - The ActivityPub private-url docloader patch (
scripts/patch-endpoint-activitypub-private-url-docloader.mjs) allows Fedify lookups for your own publication hostname when split-horizon DNS resolves it to a private jail IP. - The ActivityPub locale patch creates/repairs
locales/de.jsonfromlocales/en.jsonso backend UI keys do not render as rawactivitypub.*translation strings whenSITE_LOCALE=de. - Quick verification commands:
curl -s "https://blog.giersig.eu/.well-known/webfinger?resource=acct:<handle>@blog.giersig.eu" | jq .curl -s -H "Accept: application/activity+json" "https://blog.giersig.eu/" | jq .curl -s "https://blog.giersig.eu/nodeinfo/2.1" | jq .- If a reverse proxy serves static HTML, ensure AP requests are proxied to Indiekit for
/activitypub*,/.well-known/*,/nodeinfo/*, and content-negotiatedAccept: application/activity+json/application/ld+jsonrequests on/and post URLs.
Startup script
start.shis intentionally ignored by Git (.gitignore) so server secrets are not committed.- Use
start.example.shas the tracked template and keep real credentials in environment variables (or.envon the server). - Startup scripts parse
.envwith thedotenvparser (not shellsource), so values containing spaces are handled safely. start.example.shincludes an optional background webmention sender polling loop for bare-metal deployments (including FreeBSD jails).- For FreeBSD service management, use
indiekit.rcd.exampleas a template for/usr/local/etc/rc.d/indiekit. - Important: do not use
daemon -rin the rc.d command args. Letservice indiekit restartcontrol restart behavior;-rcan keep the supervisor alive during stop/restart. - The rc.d template uses daemon supervisor pidfile
-P(and child pidfile-p) and supportsindiekit_stop_timeoutinrc.conf(default20seconds). - FreeBSD rc.d install example:
install -m 0555 /usr/local/indiekit/indiekit.rcd.example /usr/local/etc/rc.d/indiekit
sysrc indiekit_enable=YES
service indiekit restart
- FreeBSD jail env example for auto-send polling:
SITE_URL=https://blog.example.net
PORT=3000
WEBMENTION_SENDER_AUTO_POLL=1
WEBMENTION_SENDER_POLL_INTERVAL=300
WEBMENTION_SENDER_HOST=127.0.0.1
WEBMENTION_SENDER_PORT=3000
WEBMENTION_SENDER_MOUNT_PATH=/webmention-sender
# Optional overrides
# WEBMENTION_SENDER_ORIGIN=https://blog.example.net
# WEBMENTION_SENDER_ENDPOINT=http://127.0.0.1:3000/webmention-sender
- Startup scripts run preflight + patch helpers before boot (
scripts/preflight-production-security.mjs,scripts/preflight-mongo-connection.mjs,scripts/preflight-activitypub-rsa-key.mjs,scripts/preflight-activitypub-profile-urls.mjs,scripts/patch-lightningcss.mjs,scripts/patch-endpoint-media-scope.mjs,scripts/patch-endpoint-media-sharp-runtime.mjs,scripts/patch-frontend-sharp-runtime.mjs,scripts/patch-endpoint-files-upload-route.mjs,scripts/patch-endpoint-files-upload-locales.mjs,scripts/patch-endpoint-activitypub-locales.mjs,scripts/patch-endpoint-activitypub-docloader-loglevel.mjs,scripts/patch-endpoint-activitypub-private-url-docloader.mjs,scripts/patch-endpoint-activitypub-migrate-alias-clear.mjs,scripts/patch-endpoint-homepage-locales.mjs,scripts/patch-frontend-serviceworker-file.mjs,scripts/patch-endpoint-comments-locales.mjs,scripts/patch-conversations-collection-guards.mjs,scripts/patch-indiekit-routes-rate-limits.mjs,scripts/patch-indiekit-error-production-stack.mjs,scripts/patch-indieauth-devmode-guard.mjs,scripts/patch-listening-endpoint-runtime-guards.mjs). - The production security preflight blocks startup on insecure auth/session configuration and catches empty-password bcrypt hashes.
- One-time recovery mode is available with
INDIEKIT_ALLOW_PASSWORD_SETUP=1to bootstrap/resetPASSWORD_SECRETwhen locked out. Remove this flag after setting a valid hash. - The media scope patch fixes a known upstream issue where file uploads can fail if the token scope is
create update deletewithout explicitmedia. - The ActivityPub RSA key preflight repairs or creates a usable
type="rsa"key document inap_keys, so outgoing federation requests can be signed and accepted by stricter inboxes. - The ActivityPub profile URL preflight repairs invalid URL fields in the
ap_profiledocument (for example relativeiconpaths), preventing/.well-known/webfingerand actor responses from failing withTypeError: Invalid URL. - The media sharp runtime patch makes image transformation resilient on FreeBSD: if
sharpcannot load, uploads continue without resize/rotation instead of crashing the server process. - The frontend sharp runtime patch makes icon generation non-fatal on FreeBSD when
sharpcannot load, preventing startup crashes in asset controller imports. - The files upload route patch fixes browser multi-upload by posting to
/files/upload(session-authenticated) instead of direct/mediacalls without bearer token. - The files upload locale patch adds missing
files.upload.dropText/files.upload.browse/files.upload.submitMultiplelabels in endpoint locale files so UI text does not render raw translation keys. - The ActivityPub locale patch backfills missing
delocale keys from the endpoint'senlocale and applies German admin title labels for notifications/profile. - The comments locale patch backfills missing comments endpoint locale files, adds translations for de/es/fr/nl/pt/sv, and localizes dashboard labels that were hardcoded in the comments template.
- The frontend serviceworker patch ensures
@indiekit/frontend/lib/serviceworker.jsexists at runtime, forces network-only handling for/authand/sessionpages, patches frontend layout templates to unregister stale service workers and clear caches on load, and suppresses sidebar rendering wheneverapp--minimaluiis present. - The conversations guard patch prevents
Cannot read properties of undefined (reading 'find')when theconversation_itemscollection is temporarily unavailable. - The indiekit routes rate-limit patch (ported from
rmdes/indiekit-cloudron) keeps strict limits on/session/*, applies generous limits to public API/well-known routes, and removes extra rate limiting from authenticated routes to avoid admin-side 429 spikes. - The indiekit error stack patch (ported from
rmdes/indiekit-cloudron) suppresses stack traces in production error pages/JSON responses to avoid leaking internal runtime details. - The indieauth dev-mode guard patch prevents accidental production auth bypass by requiring explicit
INDIEKIT_ALLOW_DEV_AUTH=1to enable dev auto-login, and broadens safe local redirect validation to allow common path characters (-,.,%) used by routes such as/auth/new-password.
AI transparency
AI disclosure metadata is captured per-post and surfaced in the blog's frontend as a badge, a sidebar widget, and a full /ai/ stats page.
Frontmatter fields
Four optional fields are stored under the ai: key in each post's frontmatter:
ai:
textLevel: "0" # 0 = none, 1 = editorial, 2 = co-drafted, 3 = AI-generated
codeLevel: "0" # same scale, optional
tools: "" # comma-separated tool names, optional
description: "" # free-text disclosure note, optional
Articles and notes support all four fields. Other post types (bookmarks, likes, etc.) do not.
Backend fields (Micropub form)
scripts/patch-endpoint-posts-ai-fields.mjs patches the Nunjucks templates inside @indiekit/endpoint-posts to add aiTextLevel, aiCodeLevel, aiTools, and aiDescription inputs to the article/note edit form. scripts/patch-endpoint-posts-ai-cleanup.mjs patches form.js in the same endpoint to strip empty AI fields from the Micropub payload before submission so unused optional fields are not written as empty strings.
Frontmatter generation (preset-eleventy patch)
scripts/patch-preset-eleventy-ai-frontmatter.mjs patches post-template.js inside @rmdes/indiekit-preset-eleventy. The patch adds a block that writes the ai: YAML section from the JF2 aiTextLevel/aiCodeLevel/aiTools/aiDescription properties when converting a Micropub post to markdown frontmatter.
Root cause of the v4 fix: @indiekit/endpoint-micropub/lib/utils.js — getPostTemplateProperties() — explicitly deletes post-type before calling postTemplate(). Earlier patch versions (v1–v3) relied on properties["post-type"] or properties.postType to detect whether a post is an article or note, so supportsAiDisclosure was always false and the ai: block was never written. The v4 fix detects post type from properties.permalink via URL path regex instead:
const permalink = String(properties.permalink ?? "");
const supportsAiDisclosure =
postType === "article" || postType === "note" ||
/\/articles(?:\/|$)/.test(permalink) || /\/notes(?:\/|$)/.test(permalink);
The patch script is idempotent and versioned: it detects the current patch level (v1/v2/v3/upstream) by matching a unique marker string and upgrades to v4 in place.
Blog frontend
_includes/layouts/post.njk— renders an AI disclosure badge below article/note content, readingai.textLevelandai.codeLevelfrom the post's frontmatter._includes/components/widgets/ai-usage.njk— compact sidebar widget showing totals, level breakdown, and a per-year contribution graph. Hidden when no posts have AI metadata._includes/components/sections/ai-usage.njk— full-width homepage section version of the same stats.eleventy.config.js— definesaiStatsandaiPostsEleventy filters that scancollections.postsforai.textLevelvalues to compute totals, percentages, and by-level counts.
Re-saving existing posts
Posts published before the v4 fix lack the ai: frontmatter block. To add it, open each post in the Indiekit backend (/posts) and save it again without changes. The patched postTemplate() will then write the ai: block with default values (textLevel: "0", codeLevel: "0"). AI level values previously entered in the form will now be persisted correctly on save.