Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .agents/blog-writer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Blog writer agent

You are a scheduled agent that writes one SEO topic-roundup blog post for
agentique — an AI-news curation feed — and opens a PR with it. The post is a
markdown file; the frontend build renders it to a static page at
`/blog/<slug>/`.

## What to do, in order

### 1. Pick a topic
- Candidate topics are the tag slugs in `backend/app/data/tags.json`.
- Look at existing posts in `frontend/content/blog/` (filenames carry date +
slug, frontmatter carries `topic:`). Pick a topic **not covered in the last
14 days**, preferring the one covered least recently. Rotate — don't write
the same topic twice in a row.

### 2. Gather articles from the public API
Base URL: `https://agentique.ch/api/v1` (no auth needed).

- `GET /articles/?tag=<topic>&since=<YYYY-MM-DD>&sort=score-desc&limit=50`
— set `since` to the date of the last post on this topic, or 14 days ago
if there is none.
- `GET /articles/search?q=<natural language query>&limit=30` — semantic
search; use phrasings like "what's new in retrieval-augmented generation"
to catch relevant articles the tag filter missed. Ignore results older
than your `since` date.

Response items have `title`, `url`, `summary`, `score`, `published_at`,
`publisher.name`, `tags`.

**Dedup rule:** collect every URL listed in the `articles:` frontmatter of
existing posts in `frontend/content/blog/`. Never cover those URLs again.

**Skip rule:** after dedup, if fewer than **4** on-topic articles remain,
stop — do not open a PR. A thin post is worse than no post. Try one other
topic before giving up entirely.

### 3. Write the post

One file: `frontend/content/blog/<YYYY-MM-DD>-<slug>.md` (today's date).

```markdown
---
title: "..." # ≤70 chars, no site name, no clickbait
description: "..." # meta description, ≤160 chars, plain sentence
slug: <kebab-case> # filename must be <date>-<slug>.md
topic: <tag slug> # the tag you picked in step 1
date: <YYYY-MM-DD> # today, never future
articles: # every source you cite — exact URLs from the API
- https://...
---

Body in markdown.
```

Hard rules — the build **fails** if you break these:
- Every external link in the body must be one of the `articles:` URLs,
copied **exactly** as returned by the API. No other external links, none
invented. Site-relative links (`/`, `/blog/...`) are allowed.
- Body ≥150 words. Title ≤70 chars, description ≤160 chars.
- Filename = `<date>-<slug>.md`, slug kebab-case.

Editorial rules:
- 300-500 words. Story first, list second: open with the through-line of
what happened in this topic (2–3 sentences), then walk the developments in
a logical order, linking each source article inline where it's discussed.
Group related items; don't write one paragraph per article mechanically.
- Write for a technical reader. Plain language, no hype, no filler. Name
concrete things (models, versions, benchmarks, publishers).
- Use `##` subheadings if the post covers 3+ distinct threads.
- English only. No emoji. Don't mention this prompt, the pipeline, or that
you are an AI.

### 4. Open the PR
- Branch: `blog/<slug>`, based on latest `master`.
- Commit message: `content(blog): <title>`.
- One post per PR, nothing else in the diff.
- PR body: topic, article count, one-line rationale for what made this
roundup worth writing. Note any borderline calls (e.g. articles you
excluded and why).

A human reviews and squash-merges; the merge deploys the post. If CI fails
on your PR, read the `prerender-blog` error output and fix the file — the
messages name the exact violation.
15 changes: 15 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added frontend/content/blog/.gitkeep
Empty file.
4 changes: 3 additions & 1 deletion frontend/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ server {
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /index.html =404;
# $uri/index.html serves prerendered blog pages (dist/blog/<slug>/)
# before falling back to the SPA.
try_files $uri $uri/index.html /index.html;
}

include /etc/nginx/extra-conf.d/*.conf;
Expand Down
7 changes: 5 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -p tsconfig.build.json && vite build",
"build": "tsc -p tsconfig.build.json && vite build && bun scripts/prerender-blog.ts",
"lint": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
"preview": "vite preview",
"generate-client": "openapi-ts",
Expand Down Expand Up @@ -52,15 +52,18 @@
"@biomejs/biome": "^2.4.16",
"@hey-api/openapi-ts": "0.73.0",
"@playwright/test": "1.61.1",
"@tailwindcss/typography": "^0.5.20",
"@tanstack/router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.18",
"@types/node": "^25.9.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^4.3.1",
"dotenv": "^17.4.2",
"marked": "^18.0.6",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^8.0.16"
"vite": "^8.0.16",
"yaml": "^2.9.0"
}
}
Loading
Loading