API Endpoints
All endpoints are under /api/. They accept and return JSON. Path traversal is sanitized on every endpoint that takes a filename or slug.
POST /api/sync
Write HTML content to a file on disk.
Request body
{ "code": "<html>...", "slug": "my-file" }The slug is sanitized via sanitizeSlug(). If it has no month prefix, the current month is prepended. If it already has a valid YYYY-MM/name prefix, it is used as-is.
Response (200)
{ "ok": true, "slug": "2026-05/my-file" }Response (400) — missing or invalid fields.
Response (500) — filesystem error.
GET /api/files
List all saved files.
Response (200)
{
"files": [
{ "slug": "2026-05/my-file", "name": "my-file", "month": "2026-05", "size": 1024 },
...
]
}Files are sorted alphabetically within each month. Months are sorted newest-first.
POST /api/new
Create a new file with the default starter template and return its slug.
Request body — empty or {}.
Response (200)
{ "slug": "2026-05/untitled-1746441600000" }The timestamp in the name is Date.now() at the time of creation.
POST /api/rename
Rename an existing file. Keeps the same month directory.
Request body
{ "oldSlug": "2026-05/old-name", "newName": "new-name" }newName is the bare name without month prefix or .html extension.
Response (200)
{ "slug": "2026-05/new-name" }Response (400) — if oldSlug or newName is invalid, or if the source file does not exist.
Response (409) — if a file with newName already exists in the same month.
POST /api/clone
Duplicate a file. Resolves name conflicts automatically.
Request body
{ "slug": "2026-05/my-file" }Response (200)
{ "slug": "2026-05/my-file-copy" }If my-file-copy exists, tries my-file-copy-2, my-file-copy-3, and so on.
Path sanitization
sanitizeSlug(input) in src/lib/store.ts:
- Strips leading slashes and
.htmlsuffix. - Splits on
/. Rejects inputs with 0 or more than 2 parts. - If two parts, validates the first as
YYYY-MM. If one part, prefixes current month. - Replaces anything outside
[a-zA-Z0-9._-]in the name with-, then strips leading/trailing hyphens.
Any input that doesn't survive these steps returns null and the endpoint responds with 400.