This is the Next.js website for Mellea — the landing page and developer blog deployed at mellea.ai. For the Mellea Python library itself, see the mellea repository.
Adding or editing a blog post → go to § Adding Blog Posts. No dev environment, no code changes needed.
Adding a news highlight → go to § Adding News Items. No dev environment, no code changes needed.
Changing the site (UI, components, CI, dependencies) → read everything below.
npm install
npm run dev # http://localhost:4000
npm run lint # ESLint
npm run lint:md # Markdown lint (content files)
npm run typecheck # tsc --noEmit
npm run test:unit # Vitest (no browser required)
npm run test:e2e # Playwright (auto-starts dev server)
npm run build # Static export to ./out/Branches: feat/topic, fix/description, docs/topic
| Path | Contents |
|---|---|
src/app/ |
Next.js App Router pages and layouts |
src/components/ |
React components |
src/lib/ |
Server-side utilities (blog parsing, etc.) |
src/config/ |
Site-wide configuration (site.ts) |
content/blogs/ |
Markdown blog posts with YAML front matter |
content/news/ |
Markdown news/highlights items with YAML front matter |
public/ |
Static assets (images, CNAME) |
tests/unit/ |
Vitest unit tests |
tests/e2e/ |
Playwright E2E tests |
.github/workflows/ |
CI pipeline |
- TypeScript throughout — no
anywithout a comment explaining why - Server Components by default; add
'use client'only when needed src/lib/blogs.tsuses Node.jsfs— never import it in Client Componentsparamsin Next.js 15 page components is aPromise— alwaysawait params- No CSS modules, no Tailwind — all styles in
src/app/globals.css src/config/site.tsis the single source of truth for URLs and repo slug
Do not add AI assistant attribution to commits, PRs, code, or documentation unless explicitly asked. No Co-Authored-By lines, no AI-generated footers.
Plain descriptive messages: fix: nav link selector in E2E tests, feat: add tags filter to blog listing, docs: update CONTRIBUTING.md.
No Angular-style mandatory types required, but keep messages short and imperative.
Run the appropriate checks before every commit. CI will reject failures; fixing them after the fact wastes pipeline time.
npm run lint # must be clean
npm run typecheck # must be clean
npm run test:unit # must pass
npm run test:e2e # must passIf you rename or remove a CSS class, check tests/e2e/ for selectors that reference it and update them in the same commit.
- No new
anytypes without a comment explaining why - No hardcoded URLs — use
src/config/site.ts - External links in Markdown? CI runs lychee — broken links block deploy
Next.js 15 App Router, fully static (output: 'export'). Nothing runs at request time — all pages are pre-rendered at build time or handled client-side.
- No
next/headers, no route handlers, no server actions Imagecomponent usesunoptimized: true(required for static export)trailingSlash: trueis set innext.config.mjsparamsin page components is aPromisein Next.js 15 — alwaysawait paramssrc/lib/blogs.tsuses Node.jsfs— Server Components only, never import in Client Components
- Build-time (Server Components):
getAllBlogs()→ landing page + blog listing;getAllBlogSlugs()+getBlog(slug)→ individual post pages;getAllNews()→ landing page news highlights - Client-side (Client Components): GitHub API stats via
useGitHubStatshook on mount; image compare slider viareact-compare-slider
- Single global CSS file:
src/app/globals.css - IBM Plex Sans + IBM Plex Mono (Google Fonts)
- Dark/light theme via CSS custom properties +
@media (prefers-color-scheme: light) - No CSS modules, no Tailwind
Pushing to main triggers .github/workflows/nextjs.yml. Pipeline: lint → (test-unit ∥ build) → test-e2e → deploy. Static artifact produced by next build with output: 'export', deployed to GitHub Pages.
Drop a .md file in content/blogs/. The filename becomes the URL slug: my-post.md → /blogs/my-post.
Naming rules — the filename is a permanent public URL; once published it cannot be changed without breaking existing links (this site has no redirect support).
- Use the topic as the slug, not a description of the content type:
llm-provider-failover.md, notblog-llm-provider-failover.md - Never prefix with
blog-,post-,article-, or similar — the/blogs/path already provides that context - Append
-melleaonly if needed to disambiguate from a generic topic name
Required front matter:
---
title: "Your Post Title"
date: "YYYY-MM-DD"
author: "Your Name"
excerpt: "One sentence shown on the blog listing and cards."
tags: ["tag1", "tag2", "etc"]
---| Field | Required | Notes |
|---|---|---|
title |
Yes | Post title |
date |
Yes | YYYY-MM-DD, used for sorting |
author |
Yes | Display name |
excerpt |
Yes | Shown on cards and listing page |
tags |
No | Array of strings |
Set date to a future date matching when the post will go live, not when it was drafted — PRs typically take days to review.
Use US spelling throughout (color not colour, organized not organised). This applies regardless of the author's locale — consistency across posts matters more than author preference.
Publish-date reminder — include this line in the PR description, matching the front matter date:
/remind 2026-05-15
A workflow runs daily at ~9am Eastern and posts a reminder comment on the PR once the date arrives, asking the author to enable auto-merge. The PR should be approved by then so it can be enqueued immediately.
Verify with npm run build — no config changes or code edits needed.
Drop a .md file in content/news/. News items appear in the "Latest News" highlights strip on the landing page hero section, sorted by date descending. Unlike blog posts, news items link to an external URL (opened in a new tab) — they do not have their own page on the site.
Required front matter:
---
title: "Short Headline"
date: "YYYY-MM-DD"
category: "Release"
excerpt: "One sentence description shown on the card."
url: "https://example.com/full-link"
source: "GitHub"
---| Field | Required | Notes |
|---|---|---|
title |
Yes | Short headline for the card |
date |
Yes | YYYY-MM-DD, used for sorting |
category |
Yes | One of: Release, Event, Integration, Community, Feature |
excerpt |
Yes | One sentence shown on the card |
url |
Yes | External link (must be a full URL) |
source |
No | Display label for the link (e.g. "GitHub", "PyCon"); defaults to "Read more" |
The category field controls the visual styling — each category gets a distinct accent color on the card's left border and category badge. No markdown body content is needed (only frontmatter is used).
Verify with npm run build — no config changes or code edits needed.
| Problem | Fix |
|---|---|
params type error in page component |
params is Promise<{slug: string}> in Next.js 15 — use await params |
| E2E test strict mode violation | Scope selector (e.g. page.getByRole('banner').getByRole('link', ...)) |
fs import error in client bundle |
Move the import to a Server Component; never import src/lib/blogs.ts client-side |
| ESLint config error | Uses ESLint 9 flat config (eslint.config.mjs) — no legacy .eslintrc |