---
title: "Guide for agents: Espresso Code: Italian Dev Insights"
lang: "en"
translations:
  it: "https://blog.sf-paris.dev/agenti.md"
---

# Guide for agents: Espresso Code: Italian Dev Insights

How to read Espresso Code: Italian Dev Insights (https://blog.sf-paris.dev/) from a program and, with an account, prepare drafts. The blog is bilingual (languages: en, it); each article can have a translation in the other language. This guide contains no credentials: write access is granted by the blog owner.

## Reading without an account

- **Markdown of every article**: the article URL with `.md` instead of the trailing slash. Example: https://blog.sf-paris.dev/temu-shein-3-euro-duty-2-euro-fee.md (HTML page: https://blog.sf-paris.dev/temu-shein-3-euro-duty-2-euro-fee/). Static pages work the same way; a language home that is a static page is `/index.md` or `/it/index.md`.
  - Same content with `?format=markdown` on the HTML URL, or with the `Accept: text/markdown` header (HTML responses send `Vary: Accept`).
  - Response `text/markdown; charset=utf-8` with `Link: <HTML page>; rel="canonical"`, `X-Robots-Tag: noindex` (the HTML is the canonical document), `ETag` and `Last-Modified` for conditional requests (304).
  - It starts with YAML front matter: `title`, `url`, `lang`, `translations` (language: URL), `author`, `published`, `updated`, `categories`, `tags`, `summary`, `reading_time_minutes`, `word_count`. Then the title and the body: headings, lists, code with its language, GFM tables, images with alt text, FAQs as `<details><summary>`.
  - Drafts, password-protected content and unknown URLs answer 404.
- **Full list**: https://blog.sf-paris.dev/llms-full.txt (published articles per language: title, `.md` URL, date, summary; never drafts or noindex content). Short index: https://blog.sf-paris.dev/llms.txt.
- **RSS feeds per language**:
  - `en`: https://blog.sf-paris.dev/feed/
  - `it`: https://blog.sf-paris.dev/it/feed/
- **Sitemap**: https://blog.sf-paris.dev/sitemap.xml
- **WordPress REST API** (public, read only): https://blog.sf-paris.dev/wp-json/wp/v2/posts; pick the language with `?lang=it` or `?lang=en`. Every article points to its JSON with `<link rel="alternate" type="application/json">`.
- Every article page also has `<link rel="alternate" type="text/markdown">` in its head.

## Abilities (with an account)

Operations are abilities of the WordPress Abilities API, category `espresso`. All of them need an authenticated WordPress user; each one checks that user's capabilities.

| Ability | REST method | Permission | What it does |
| --- | --- | --- | --- |
| `espresso/list-posts` | GET | `read` (+ `edit_posts` for unpublished) | List posts or pages with filters (lang, status, category, search, paging). |
| `espresso/get-post` | GET | `read_post` | One post by id or URL: metadata, Markdown (or block) content, SEO fields, translations. |
| `espresso/seo-audit` | GET | `edit_posts` (published) / `edit_post` | Read-only SEO/GEO checks with score and concrete fixes. |
| `espresso/create-draft` | POST | `edit_posts` | Create a draft (block markup or Markdown); never publishes; idempotency key. |
| `espresso/update-post` | POST | `edit_post` (+ `publish_posts` beyond draft/pending) | Update fields of a post you can edit; revision first; `dry_run` returns the diff. |
| `espresso/link-translation` | POST | `edit_post` on both posts | Link two posts as Polylang translations (guarded, idempotent). |

- List and JSON schemas: `GET https://blog.sf-paris.dev/wp-json/wp-abilities/v1/abilities?category=espresso` (each ability has `input_schema` and `output_schema`).
- Run: `GET https://blog.sf-paris.dev/wp-json/wp-abilities/v1/abilities/espresso/list-posts/run?input[lang]=en&input[per_page]=5` for read-only ones; `POST https://blog.sf-paris.dev/wp-json/wp-abilities/v1/abilities/espresso/create-draft/run` with JSON body `{"input": {"title": "…", "content": "## Heading\n\nText in Markdown…", "lang": "en", "idempotency_key": "pipeline-2026-10-31-01"}}` for the others.
- SEO fields (title, description, keyphrase) are read with `espresso/get-post` and written with the AIOSEO ability `aioseo-posts/seo-data-update`.
- Rules: agents create **drafts**; a person publishes. `espresso/update-post` stores a revision before writing, only moves status between draft and pending without `publish_posts`, never deletes; with `"dry_run": true` it returns the diff without saving (or creating tags), and with `expected_modified_gmt` it does not overwrite someone else's changes (409). Translations: a draft in the other language, then `espresso/link-translation`.
- Errors: `WP_Error` with a code (`espresso_not_found` 404, `espresso_forbidden` 403, `espresso_conflict` 409, `espresso_invalid` 400, `ability_invalid_input` 400 for input outside the schema) and a message.

## MCP

When the official MCP Adapter plugin is active on the site, abilities flagged `meta.mcp.public` (read-only ones and `espresso/create-draft`) are reachable through the default MCP server: https://blog.sf-paris.dev/wp-json/mcp/mcp-adapter-default-server (Streamable HTTP; tools `mcp-adapter-discover-abilities`, `mcp-adapter-get-ability-info`, `mcp-adapter-execute-ability`). The others stay REST only. A 404 on that URL means the adapter is not installed yet.

## Statistics

When measurement is on (pages load the plugin's `misura.js`), the blog counts visits with self-hosted Umami, from its own domain (`/_s/script.js`, `/_s/api/send`): no cookies, no third parties, the IP is shortened before Umami, which does not store it. Not measured: signed-in users (agents with an account included), previews, feeds, REST and the responses for agents (`.md`, `?format=markdown`, `Accept: text/markdown`, llms-full.txt, this guide); nor readers who opted out (`umami.disabled` in localStorage) or send Global Privacy Control or Do Not Track.

- **Views**: URL without query or `#`; the Umami tag is the format for articles (`ristretto`, `espresso`, `lungo`, `doppio`, `moka`, from the reading time) and the page type elsewhere (`home`, `archivio`, `pagina`; `articolo` for a password-protected article). Search and 404 are events, not views; no URL carries the query, the `#` or the search term (`/search/<term>/` is cut to `/search/`).
- **Events** (Italian names, stable): `letto` (end of the text seen and at least 15 s of visible page, once, not on password-protected articles; with `secondi`), `commento`, `copia-codice` (`linguaggio`), `copia-link`, `link-esterno` (`host`), `lingua` (`da`, `verso`), `ricerca` (`lingua`, `risultati`, `termine` only when short and not looking like personal data, otherwise `escluso`), `404` (`lingua`, `path`).
- **Page properties** (with `letto` and `commento`): `tipo` (`articolo`, `home`, `archivio`, `pagina`, `ricerca`, `404`), `lingua`, and for articles `formato`, `minuti`, `categoria`, `mesi` (since publication).
- **Reading the data**: through Umami's MCP endpoint, `https://stats.sf-paris.dev/api/mcp`, with an API key from the blog owner; it answers only from the authorized network. There is no WordPress ability for statistics.

## Authentication

- WordPress **Application Passwords**, over HTTPS only: header `Authorization: Basic base64(user:application-password)`. One password per agent or service, named after who uses it; revoke it from the user profile.
- **Dedicated user with a minimal role**: reading and drafting need only `read` and `edit_posts` (Contributor or a custom role). Never an administrator: an application password inherits every capability of its user.
- **Two-Factor**: for users with two-step verification, the Two-Factor plugin blocks API login with the regular password; application passwords keep working (since version 0.17). Only the `two_factor_user_api_login_enable` filter changes this.
- Tokens of the blog's internal editorial API are something else and are not accepted here.

Versione italiana: https://blog.sf-paris.dev/agenti.md
