agentleFS
fjrevoredofjrevoredo/mini-diarium/website/CLAUDE.md

mini-diarium / website

CLAUDE.md318 starsChanged 12 days agoMITView on GitHub

For project architecture and cross-cutting conventions see the root CLAUDE.md. Static marketing site — plain HTML/CSS/JS served via Nginx. No frontend framework, no build step for the site itself. Blog posts and documentation pages both require generation — never edit their HTML output directly. Both the blog and the docs sections are generated from Markdown sources. Editing the HTML output directly will be silently overwritten on the next build run. The only files you should edit manually are: - posts-src/*.md…

The file

Show its rules and text

We split this CLAUDE.md into the rules it gives an agent

21 rules. About 0 are common, 9 are distinctive, and 12 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 12

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When editing any of the static manual pages above, ensure these elements stay consistent:

    line 34
  2. Very specific:

    Add a file to posts-src/ named YYYY-MM-DD-slug.md.

    line 45
  3. Very specific:

    Always use website:build-static, not website:blog alone.

    line 75
  4. Distinctive:

    After the build completes, run the following checks before committing:

    line 92
  5. Very specific:

    All internal links in the article (/encrypted-journal/, /compare/, /blog/...) resolve to existing files on disk

    line 103
  6. Very specific:

    No em dashes (—) in the source Markdown (grep for — in the .md file)

    line 104
  7. Very specific:

    Delete the .md file from posts-src/ and run bun run website:build-static.

    line 122
  8. Distinctive:

    Every blog post should target at least one specific search query.

    line 148
  9. Distinctive:

    Before writing a new post:

    line 155
  10. Very specific:

    Read STRATEGY.md §3 to place the post in a cluster and confirm the target query is not already the title/H1 of an existing post …

    line 156
  11. Very specific:

    Check live positions in the latest export under docs/seo/performance/ (do not rely on inline numbers anywhere.

    line 158
  12. Distinctive:

    Avoid titles that read as feature lists or specifications (e.g., "App for X, Y, and Z").

    line 173
  13. Distinctive:

    Use hooks: numbers ("5 Things"), years ("in 2026"), questions, or "Why X" / "How to X" patterns

    line 174
  14. Very specific:

    Always link to at least two of /encrypted-journal/, /compare/, or a related blog post.

    line 195
  15. Distinctive:

    When creating a new static HTML page (not generated from Markdown), ensure:

    line 207
  16. Distinctive:

    When changing a post's description, update both maps: If the slug doesn't exist in these maps, the generator falls back to the front matter …

    line 238
  17. Very specific:

    Regenerating the key: Delete the old website/indexnow-key-*.txt, generate a new 32-char hex key (node -e …

    line 281
  18. Very specific:

    After adding images, verify every <img src="..."> and <figure>'s image path in the built website/docs/*/index.html resolves to a real file …

    line 318
  19. Distinctive:

    When implementing a UI pattern the user names by reference ("like the dropdown on X"), screenshot that reference before writing CSS, and …

    line 346
  20. Very specific:

    Use the standard docker compose commands from the website/ directory to build and preview the site locally before committing or deploying.

    line 392
  21. Very specific:

    Look at the screenshot with the Read tool before deciding the change matches the request — reasoning from CSS values on paper is not a …

    line 428

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

Website (website/) — Fenmark

For project architecture and cross-cutting conventions see the root CLAUDE.md.

Static marketing site — plain HTML/CSS/JS served via Nginx. No frontend framework, no build step for the site itself. Blog posts and documentation pages both require generation — never edit their HTML output directly.

Key Rule: Never Edit Generated HTML Directly

Both the blog and the docs sections are generated from Markdown sources. Editing the HTML output directly will be silently overwritten on the next build run.

Source (edit these)Generated output (never edit)
posts-src/*.mdblog/*/index.html, blog/index.html, blog/feed.xml, sitemap.xml, llms.txt
docs-src/*.mddocs/*/index.html, docs/index.html, docs/*.md (per-page Markdown mirror), llms-full.txt

The only files you should edit manually are:

  • posts-src/*.md — blog post sources (the canonical input)
  • docs-src/*.md — documentation page sources (the canonical input)
  • website/index.html — homepage (between its static sections, not the blog teaser block)
  • encrypted-journal/index.html — the encrypted journal guide page
  • compare/index.html — the comparison matrix page
  • privacy/index.html — the privacy policy page
  • newsletter/index.html — the newsletter signup page
  • donate/index.html — the donation page
  • css/, js/ — styles and scripts

Each static manual page has an updated date in its STATIC_PAGES entry in scripts/generate-website-blog.mjs (the homepage uses the separate HOMEPAGE_UPDATED constant near INDEX_PATH) — this drives its sitemap.xml lastmod, deliberately decoupled from file mtime so an unrelated CSS/JS rebuild doesn't bump it. Whenever one of these pages' actual content changes, bump its updated constant (or HOMEPAGE_UPDATED) in the same change, the same way updated: front matter is bumped for blog posts and docs pages.

When editing any of the static manual pages above, ensure these elements stay consistent:

  • Navigation — every manual page shares the same nav structure. If you add or remove a nav item, update all of them plus the generator's buildNav() in scripts/generate-website-blog.mjs.
  • Footer — the footer is duplicated the same way the nav is: once per manual page, plus buildFooter() in both scripts/generate-website-blog.mjs and scripts/generate-website-docs.mjs (those two are byte-identical, so any footer edit goes into both). Changing a footer link means editing every manual page and both generators, then re-running bun run website:build-static so blog/ and docs/ pick it up.
  • Hreflang — every page must have <link rel="alternate" hreflang="x-default" href="..."> pointing to its canonical URL. The generators handle this for blog and docs pages automatically.

Adding a Blog Post

Step 1 — Create the Markdown source

Add a file to posts-src/ named YYYY-MM-DD-slug.md. Use _template.md as a starting point.

Required front matter fields (all must be present):

---
title: The article title
slug: the-stable-url-slug
description: Meta description used for search and social previews (1–2 sentences).
date: 2026-04-05
updated: 2026-04-05
author: Francisco J. Revoredo
tags: tag one, tag two, tag three
excerpt: Optional shorter summary for cards and index. Falls back to description if omitted.
draft: true
---
  • slug must be globally unique across all posts — the script throws on duplicates.
  • date and updated must be YYYY-MM-DD. The script validates this strictly.
  • tags is a comma-separated list; at least one tag is required.
  • draft: true excludes the post from all generated output. Remove or set to false when ready to publish.
  • excerpt is optional. If omitted, description is used for blog index cards.

Step 2 — Run the full build

bun run website:build-static

Always use website:build-static, not website:blog alone. The full build runs the generator and the asset fingerprinter in sequence. Running only website:blog leaves HTML files with unfingerprinted CSS/JS references (style.css) instead of the correct content-hashed names (style.<hash>.css), which breaks cache busting in the deployed site.

This single command regenerates everything:

OutputWhat changes
blog/<slug>/index.htmlNew post HTML (created)
blog/index.htmlArticle card prepended (newest first)
blog/feed.xmlRSS item prepended
sitemap.xmlURL entry added
llms.txtArticle listed under Latest Articles
website/index.htmlBlog teaser section updated (top 3 posts)

That is the complete workflow. No further manual edits are needed.

Step 3 — Verify

After the build completes, run the following checks before committing:

Generated output (all of these must exist or be updated):

  • website/blog/<slug>/index.html was created
  • website/sitemap.xml contains https://fenmark.app/blog/<slug>/
  • website/llms.txt contains a line for the new post under "Latest Articles"
  • website/blog/feed.xml has an <item> for the new post at the top
  • website/blog/index.html has an article card for the new post
  • website/index.html homepage teaser is updated (it reflects the 3 newest posts)

Content correctness:

  • All internal links in the article (/encrypted-journal/, /compare/, /blog/...) resolve to existing files on disk
  • No em dashes (—) in the source Markdown (grep for — in the .md file)
  • scripts/generate-website-blog.mjs has entries for the new slug in both DESCRIPTION_MAP and BLUF_MAP

SEO fields:

  • Title is ≤ 60 chars (count manually or check the <title> tag in the generated HTML)
  • Description is 140–160 chars (check the <meta name="description"> tag)
  • draft: false is set in front matter

Updating a Blog Post

Edit the relevant .md file in posts-src/, update the updated date, then re-run bun run website:build-static. The script regenerates all HTML from the current source state.


Removing a Blog Post

Delete the .md file from posts-src/ and run bun run website:build-static. The generator cleans up orphaned post directories automatically — it removes any blog/*/ directory that has no corresponding source file.


Front Matter Reference

FieldRequiredNotes
titleYesUsed in <title>, OG tags, breadcrumbs, article cards. Max 70 chars. Target under 60 chars for SERP display. Use specific hooks: numbers, years, questions, or concrete claims.
slugYesStable URL segment: fenmark.app/blog/<slug>/
descriptionYesMeta description + OG/Twitter description. 140–160 chars. Must be click-worthy — promise a takeaway, not a summary. The page title already summarizes; the description should make the user want to click.
dateYesPublication date (YYYY-MM-DD)
updatedYesLast modified date (YYYY-MM-DD); drives sitemap.xml lastmod
authorYesDefaults available in script; currently always Francisco J. Revoredo
tagsYesComma-separated; at least one. Rendered as tag pills and article:tag meta. Each post should target a primary keyword via its title + slug + H1, with related keywords in tags.
excerptNoShort card summary; falls back to description
draftNoSet true to exclude from all output. Omit or set false to publish.
coverImageNoFull URL to OG image; defaults to /assets/og-cover.png
canonicalNoOverride canonical URL; defaults to https://fenmark.app/blog/<slug>/

Content Strategy

Keyword Map

Every blog post should target at least one specific search query. The full keyword and topic-cluster map (pillar + clusters, target queries, current positions, owning page, and which engine surfaces the demand) lives in docs/seo/STRATEGY.md §3. That is the single source of truth; do not maintain a duplicate table here (per CONTEXT_FILES_BEST_PRACTICES.md: prefer pointers over copies).

Before writing a new post:

  • Read STRATEGY.md §3 to place the post in a cluster and confirm the target query is not already the title/H1 of an existing post (cannibalization).
  • Check live positions in the latest export under docs/seo/performance/ (do not rely on inline numbers anywhere; they are stale within days).
  • For the "before you write a blog post" checklist, follow STRATEGY.md §9.

Target topics

The owned topic and its adjacent framings are defined once in docs/seo/STRATEGY.md §1 and docs/seo/product-marketing-context.md. Place every post inside that owned topic; do not chase unrelated volume.

Title and Description Rules

  • Titles ≤ 70 chars, target ≤ 60 chars for SERP truncation safety
  • Descriptions 140–160 chars — promise a takeaway, not a summary
  • Avoid titles that read as feature lists or specifications (e.g., "App for X, Y, and Z"). Lead with the user's concern, the benefit, or a specific claim
  • Use hooks: numbers ("5 Things"), years ("in 2026"), questions, or "Why X" / "How to X" patterns
  • The title and H1 will appear in the SERP snippet — optimize for click-through, not comprehensiveness
  • After changing a post's title or description, update the corresponding entry in DESCRIPTION_MAP and BLUF_MAP in scripts/generate-website-blog.mjs or the OG/Twitter cards and "Short answer" blurb will go stale

Voice and Style

Post structure template — every blog post should follow this section order:

  1. Opening paragraph — the reader's problem or question, not the product
  2. Two to four H2 sections — each with a clear, distinct purpose
  3. "Where Fenmark fits" — factual product claims only (AES-256-GCM, no HTTP client, MIT license, key-file auth, etc.)
  4. "The practical takeaway" — one or two clear if/then recommendations plus 2–3 internal links

Prohibited patterns and voice: Follow docs/best-practices/WRITING_STYLE.md for all shared rules (em dashes, emojis, filler phrases, sentence rhythm, active voice). Blog posts add one further constraint: every claim about Fenmark must be backed by a specific technical fact. Write "encrypts each entry with AES-256-GCM before writing to disk", not "prioritizes security". If Fenmark lacks a feature the alternative has, say so plainly.

GEO (BLUF) rule:

The BLUF content shape (a self-contained 50–80-word direct answer above the first H2 that names the specific products, trade-off, and constraint) is defined in docs/seo/STRATEGY.md §2. The generator-specific mechanic: each post's BLUF_MAP entry must hold that answer and be quotable by an LLM verbatim with no surrounding context.

Internal linking:

Always link to at least two of /encrypted-journal/, /compare/, or a related blog post. Internal linking is load-bearing for the SEO/GEO strategy.

GEO (Generative Engine Optimization)

llms.txt and ai-crawlers.txt are maintained so AI crawlers index the content accurately. The generator keeps llms.txt in sync automatically. When adding a new post, the Latest Articles section in llms.txt is updated by the script — no manual edits needed.


SEO & Discoverability

Static Page Checklist

When creating a new static HTML page (not generated from Markdown), ensure:

  1. <title> under 60 chars, keyword-rich but click-worthy
  2. <meta name="description"> 140–160 chars, compelling
  3. <link rel="canonical"> self-referencing with full https:// URL
  4. <link rel="alternate" hreflang="x-default"> matching the canonical
  5. OG and Twitter card meta tags (title, description, image)
  6. JSON-LD structured data (at minimum Organization + WebPage; add FAQPage or BreadcrumbList if the content supports it)
  7. Navigation matches the existing nav structure on all other pages
  8. Added to STATIC_PAGES array in scripts/generate-website-blog.mjs (ensures inclusion in llms.txt and sitemap), including an updated: 'YYYY-MM-DD' field — required for sitemap.xml lastmod, validated by ensureDate()

Navigation Consistency

The main navigation lives in these places, which must stay in sync (the footer is duplicated across the same set):

  • website/index.html (uses #fragment links since it's at root)
  • website/encrypted-journal/index.html (uses /#fragment links)
  • website/compare/index.html (uses /#fragment links)
  • website/privacy/index.html (uses /#fragment links)
  • website/newsletter/index.html (uses /#fragment links)
  • website/donate/index.html (uses /#fragment links)
  • scripts/generate-website-blog.mjs → buildNav() (uses /#fragment links for blog pages)
  • scripts/generate-website-docs.mjs → (uses /#fragment links for docs pages)

Current nav order: Features → Security → Blog → Docs → Download

Generator Map Synchronization

scripts/generate-website-blog.mjs contains two hardcoded maps keyed by post slug:

  • DESCRIPTION_MAP — used in llms.txt entries for AI crawler summary
  • BLUF_MAP — used for the "Short answer:" blurb at the top of each blog post

When changing a post's description, update both maps. If the slug doesn't exist in these maps, the generator falls back to the front matter directly — but new posts should have explicit entries.

Monitoring Cadence

The recurring, data-driven review is the seo-performance-review skill (bi-weekly at current volume). It ingests the latest Google Search Console + Bing exports, runs the analysis framework in docs/seo/STRATEGY.md §5, produces content briefs, and updates the action plan. Run it each cycle rather than doing this by hand.

  • Export Google Search Console (3-month window, main + AI-Overview report) and Bing (overview + query-level Keyword report + AI/Copilot citations) to a new dated folder under docs/seo/performance/ each cycle. The review skill guides the pull when data is stale.
  • Compare against the most recent snapshot: the latest docs/seo/STATUS_REPORT_*.md and the prior dated folder under docs/seo/performance/ (do not inline numbers here; they go stale).
  • Key metrics to watch (baselines in the latest status report, not inlined here):
    • /encrypted-journal/ CTR (target >2%)
    • Blog post CTR at positions 6-10 (target >1%)
    • US market CTR (target >5%)
    • New blog post positions as they get indexed
  • The prioritized fix queue and the hypothesis log live in docs/seo/action-plan.md, kept current by the review skill.

Production Configuration

HTTP→HTTPS redirect is handled by Coolify, not the local website/nginx.conf. If GSC shows http:// impressions, the Coolify edge must be configured with 301 redirects.

IndexNow

IndexNow is a protocol that lets search engines (Bing, Yandex, Seznam, and others) know immediately when URLs are added, updated, or deleted. Instead of waiting for crawlers to discover changes, the site actively notifies them.

Key file: A single hex key file lives at website/indexnow-key-<HEX>.txt. The script auto-discovers it by matching website/indexnow-key-*.txt. Exactly one key file must exist — the script errors if zero or multiple are found.

Endpoints: The script submits to two endpoints in sequence — api.indexnow.org (propagates to Yandex, Seznam, etc.) and https://www.bing.com/indexnow directly. The direct Bing endpoint is required for submissions to appear in Bing Webmaster Tools; the generic endpoint alone does not register there.

Manual submission:

bun run website:submit-indexnow          # Submit all sitemap URLs to both endpoints
bun run website:submit-indexnow:dry-run  # Preview the payload without sending

CI/CD workflow: .github/workflows/indexnow.yml is triggered via workflow_dispatch (manual). Run it from the Actions tab after deploying the website. The push trigger is commented out until Coolify auto-deployment is configured — uncomment it when ready.

Regenerating the key: Delete the old website/indexnow-key-*.txt, generate a new 32-char hex key (node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"), create a new website/indexnow-key-<NEW_HEX>.txt with the key followed by a newline, and commit. No other files need updating — the script auto-discovers the key.

Response codes:

CodeMeaningAction
200URLs submitted successfullyNone
400Bad request — invalid payloadCheck the script output for details
403Forbidden — key not found on domainVerify the key file is deployed and accessible at https://fenmark.app/indexnow-key-<HEX>.txt
422URLs don't match declared hostCheck the host field in the submission payload
429Rate limitedWait and retry later

Documentation Section (docs-src/)

docs-src/ is the authoritative user-facing reference for every Fenmark feature — the primary source of truth for both users and agents auditing feature behavior. Keep it complete and up to date: whenever a user-facing feature is added, changed, or removed, update the relevant docs-src/ file in the same task. Stale docs are a bug.

Source files: website/docs-src/*.md — one file per section.

Required front matter: title, slug, description, order (integer), updated (YYYY-MM-DD), tags (comma-separated). Optional: draft: true (excludes from build).

description must be 140–160 characters. It maps directly to <meta name="description"> and is used in search snippets. "One sentence" descriptions are too short — Bing and Google will either ignore them or generate their own. Write it to be specific about what the reader will find: name the actual features, options, or answers covered, not just the topic area.

Never edit website/docs/ directly — all HTML there is auto-generated.

  • Dev iteration: bun run website:docs (docs only)
  • Full deploy build: bun run website:build-static (blog → docs → fingerprinter, in that order)

Adding Images to a Docs Page

Standard Markdown syntax works: ![alt text](src "optional caption"). The generator's image renderer (scripts/generate-website-docs.mjs, next to the heading/link overrides) wraps the output in <figure class="prose-figure"><img loading="lazy" ...></figure>, adding a <figcaption> only when the optional title (the "..." part) is present — the alt text is never duplicated into a caption. The renderer also reads the image's real pixel dimensions at build time (readImageDimensions() in scripts/website-generator-utils.mjs, supports .svg and .webp) and emits matching width/height on the <img> tag to reserve layout space and avoid CLS; it degrades to no attributes (never fails the build) if the file is missing or unrecognized. Styling lives in website/css/style.css next to the other .prose rules.

  • Screenshots: website/assets/docs/<page-slug>-<NN>-<short-description>.webp. Live-capture from the real dev app via the tauri-agent-dev skill — never reuse old promotional PNGs, they drift from the current UI. Target WebP quality 78-82. PNG is acceptable if a screenshot needs pixel-exact detail (e.g. thin text on a dense table), but WebP is the default.
  • Diagrams: website/assets/docs/diagrams/<page-slug>-<short-description>.svg. Hand-build these (not Mermaid's default theme) using the site's own dark/gold palette (--bg:#0e0e0e, --bg-card:#161616, --accent:#F5C94D, --text:#f0ede6, --text-muted:#888, --border:#2a2a2a) so they read as part of the site, not a pasted-in export. Give every SVG an explicit viewBox plus matching width/height and a system font stack — treat the first draft as a draft, not a shipped asset, and re-render it (e.g. open the raw file:// path in a browser and screenshot it) to check for label/arrow overlap before committing.
  • No fingerprinting: fingerprint-website-assets.mjs only hashes css/js. Changing an image's content later needs a new filename — overwriting one in place will not bust any cache.
  • Insert each image immediately after the paragraph or step it illustrates, not bunched at the top or bottom of the page. Don't force one image per H2 — a thin page might only need one, a long tabbed page might need several.
  • After adding images, verify every <img src="..."> and <figure>'s image path in the built website/docs/*/index.html resolves to a real file — the generator does not validate image paths, so a typo is a silently broken image on a green build.

See website/docs-src/_template.md for the starter template.

Agent-Friendly Mirrors (Copy Page, llms-full.txt)

Each docs section page (not the hub) ships a Mintlify-style "Copy page" split-button dropdown, a per-page Markdown mirror at docs/<slug>.md, and a <link rel="alternate" type="text/markdown"> discovery tag in its <head>. The button sits beside the article's first heading, not the page <h1> in the hero — see scripts/generate-website-docs.mjs for the current rendering. website/llms-full.txt is the full-text sibling of the curated llms.txt (every section's Markdown, concatenated).

  • Canonicalization, not noindex: each .md mirror is near-duplicate content of its HTML page, so nginx.conf sends an HTTP Link: <...>; rel="canonical" header pointing at the HTML page instead of blocking it with X-Robots-Tag: noindex — consolidates ranking signal per the seo-audit skill's duplicate-content guidance. Keep the header's target in sync with that section's <link rel="canonical">.
  • The three AI deep-links are unofficial (chatgpt.com/?q=, claude.ai/new?q=, perplexity.ai/search?q=) — reverse-engineered query params, not a stable contract. If one stops prefilling, drop that entry from buildAiLinks() rather than leaving a dead button. A Cloudflare-style bot-check can block a headless verification browser on one provider (seen on claude.ai) while the others pass — that's the check failing, not the link.
  • Testing "Copy page" locally: the mirror URL is always the absolute https://fenmark.app/... production URL, so a fetch from the local Docker preview is cross-origin and fails — that is expected, not a bug. Verify the fetch/guard/clipboard-write mechanism with a same-origin URL instead of chasing the cross-origin failure.

When implementing a UI pattern the user names by reference ("like the dropdown on X"), screenshot that reference before writing CSS, and verify placement in the local Docker preview rather than from container-width arithmetic on paper — a shared class (e.g. .hero-sub) can carry a rule from an unrelated page context that only looks inert until something restructures its parent.


How the build pipeline works

bun run website:build-static runs three scripts in sequence:

  1. generate-website-blog.mjs — reads all .md sources, renders HTML, updates blog/index.html, feed.xml, sitemap.xml, llms.txt, and the homepage blog teaser. Outputs unfingerprinted asset references (style.css, main.js).
  2. generate-website-docs.mjs — reads all docs-src/*.md sources, renders HTML for each section and the hub index, updates sitemap.xml and llms.txt with docs URLs. Outputs unfingerprinted asset references.
  3. fingerprint-website-assets.mjs — hashes css/style.css and js/main.js, writes the content-addressed copies (css/style.<hash>.css, js/main.<hash>.js), and rewrites all HTML references to use the new names. Removes stale fingerprinted files when the hash changes.

    Hashing is line-ending-normalized (\r\n → \n before the digest, and the hashed copy is written as those same normalized bytes), so the fingerprint is a function of the content and not of which platform checked the repo out. .gitattributes additionally pins website/ css/js/html/xml/txt output to eol=lf; the two layers are deliberate belt-and-braces — the same pairing already used for docs/diagrams/. Without them a Windows checkout produced different hashes than CI and rotated the <link>/<script> refs in every generated HTML file on each platform switch.

    The script is a no-op when nothing changed. It compares before writing and skips identical files, reporting N updated / N unchanged. A run with no CSS/JS change performs zero writes. Writes retry with backoff, because Windows AV/indexer can briefly hold a handle on a freshly written file (errno -4094 / UNKNOWN).

    Pure helpers (shortHash, readNormalized, rewriteAssetReferences, ASSETS) are exported and covered by scripts/fingerprint-website-assets.test.mjs — run it with node --test scripts/fingerprint-website-assets.test.mjs.

Do not run the scripts individually. bun run website:blog or bun run website:docs alone leaves the repo in an inconsistent state (unfingerprinted references committed alongside fingerprinted asset files).


File Layout

PathStatusPurpose
posts-src/EditBlog Markdown sources — one .md file per post
docs-src/EditDocs Markdown sources — one .md file per section (ordered by filename)
blog/, docs/GeneratedDo not edit — overwritten on every build
sitemap.xml, llms.txt, feed.xmlGeneratedDo not edit — managed by the generator scripts
index.htmlEditHomepage — edit static sections directly; blog teaser block is auto-updated
encrypted-journal/, compare/, privacy/, newsletter/, donate/EditStatic guide / comparison / policy / signup / donation pages
css/, js/EditSite stylesheet and scripts
legacy-domain/index.htmlEditSelf-contained transition page for the old mini-diarium.com homepage (inline CSS, no site JS, skipped by the fingerprinter); not in STATIC_PAGES or the sitemap
nginx.confEditShips to production (Coolify builds Dockerfile, which COPYs this to /etc/nginx/conf.d/default.conf); only redirect/TLS/canonical-host enforcement is Coolify-overridden
../docs/seo/EditSEO/GEO + growth hub (outside website/) — start at ../docs/seo/README.md

Local Docker Dev

Use the standard docker compose commands from the website/ directory to build and preview the site locally before committing or deploying. The container serves on port 80.

Verifying the site

The nginx config uses server_name fenmark.app. Browsers and curl reject requests to localhost unless you either:

  • Pass the Host header explicitly:
    curl -H "Host: fenmark.app" http://localhost:80/
    On Windows PowerShell:
    Invoke-WebRequest -Uri "http://localhost:80/" -Headers @{"Host"="fenmark.app"} -UseBasicParsing
  • Or add 127.0.0.1 fenmark.app to your hosts file (C:\Windows\System32\drivers\etc\hosts on Windows, /etc/hosts on Linux/macOS) for full browser testing.

For a browser-based screenshot check, neither trick is needed: the nginx config's second server block matches literal server_name ... localhost; too, so an automated browser (agent-browser, claude-in-chrome) can open http://localhost/docs/getting-started/ etc. directly. Reach for the Host header / hosts-file route only when a check specifically needs fenmark.app in the request (e.g. confirming the www. → apex redirect, or a header like the docs .md mirror's Link: rel="canonical" whose value is the literal production domain).

Screenshot-Verifying a Visual Change

Any task that changes rendered output (css/style.css, website/js/*.js, a generator's inline <style> or markup, a static page's HTML) must be checked with a real screenshot before it is reported done — see POST_TASK_BEST_PRACTICES.md for the gate. Mechanics:

  1. bun run website:build-static, then docker compose up --build -d from website/.
  2. Open every affected page with agent-browser (or claude-in-chrome) at http://localhost/... and screenshot it — at desktop width and at the relevant mobile breakpoint (899px for docs pages; check website/css/style.css media queries for other sections).
  3. Screenshot any interactive state the change touches (a dropdown open, a hover, a toggled modal), not just the page at rest.
  4. Look at the screenshot with the Read tool before deciding the change matches the request — reasoning from CSS values on paper is not a substitute; it produced three rounds of placement mistakes on the docs "Copy page" dropdown that a screenshot caught immediately once taken.
  5. docker compose down when done, and don't leave screenshot files committed in the repo.

Troubleshooting

SymptomFix
Port 80 already in useChange "80:80" to "8080:80" in docker-compose.yml (local testing only; revert before committing)
Stale cached layersdocker compose build --no-cache
Container starts but returns 404Check that bun run website:build-static ran and website/docs/ and website/blog/ are populated
www.fenmark.app or mini-diarium.com redirectsExpected. www.fenmark.app 301-redirects to https://fenmark.app. On the legacy mini-diarium.com / www.mini-diarium.com hosts, only / serves the self-contained transition page (legacy-domain/index.html); every other path 301-redirects to the same path on https://fenmark.app. fenmark.app/legacy-domain/ returns 404

Note: This compose file is for local preview only. nginx.conf itself ships to production via Dockerfile (Coolify builds it) — Coolify only overrides TLS termination and redirect/canonical-host enforcement on top of it. Use this compose setup to verify routing and header changes before they reach production.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.