Skip to content

Commit dc87c08

Browse files
committed
docs: update API ref, configuration, README, and CHANGELOG for v0.4.0
- docs/api.md: add settings endpoints (GET/PUT /api/admin/settings, GET /api/settings) and media endpoints (upload, list, delete, static serve note) - docs/configuration.md: add Webhook, Media, S3, and SMTP sections; update example .env with all new vars - README.md: add Media Library and Site Settings sections; update admin dashboard table and key variables table - CHANGELOG.md: write v0.4.0 entry
1 parent fb0804d commit dc87c08

4 files changed

Lines changed: 253 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## v0.4.0 (2026-04-04)
4+
5+
Media library, site settings, and S3-compatible storage.
6+
7+
- **Media library**: upload, browse, and delete images from the dashboard (`/admin/media`)
8+
- **Editor image insert**: insert images by URL from the toolbar; paste an image directly into the editor to auto-upload
9+
- **S3-compatible storage**: set `MEDIA_STORAGE=s3` to store files in AWS S3, Cloudflare R2, NevaObjects, MinIO, or any S3-compatible provider
10+
- **Site settings**: editable site metadata (`site_name`, `site_description`, `social_github`, `social_twitter`, `social_linkedin`) stored in the database and exposed via `GET /api/settings` for themes
11+
- New env vars: `SITE_URL`, `MEDIA_STORAGE`, `S3_ENDPOINT`, `S3_BUCKET`, `S3_REGION`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_PUBLIC_URL`
12+
- `docs/api.md`, `docs/configuration.md`, `README.md` updated with all new features
13+
14+
---
15+
316
## v0.3.0 (2026-04-01)
417

518
Built-in admin dashboard. No separate service or install step required.

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ Key variables:
8282
| `THEME_DIR` | `theme` | Path to the theme directory |
8383
| `THEME_BUILD_CMD` | `npm run build` | Command to rebuild the theme |
8484
| `THEME_SERVICE` | (none) | systemd service name to restart after rebuild |
85+
| `SITE_URL` | `http://localhost:8090` | Base URL used for media file URLs |
86+
| `MEDIA_STORAGE` | `local` | Storage backend: `local` or `s3` |
8587

8688
## Admin Dashboard
8789

@@ -91,11 +93,42 @@ Folio includes a built-in admin dashboard at `/admin`. No separate service or in
9193
|------|-------------|
9294
| `/admin/login` | Sign in |
9395
| `/admin/posts` | Create, edit, publish, and delete posts |
96+
| `/admin/media` | Upload, browse, and delete images |
9497
| `/admin/subscribers` | View and remove newsletter subscribers |
95-
| `/admin/settings` | Trigger a site rebuild and view build status |
98+
| `/admin/settings` | Edit site settings, trigger a rebuild, view build status |
9699

97100
The post editor uses [Milkdown](https://milkdown.dev), a WYSIWYG Markdown editor with support for headings, lists, code blocks, tables, and more. Press **Cmd+S** (or **Ctrl+S** on Windows/Linux) to save at any time.
98101

102+
## Media Library
103+
104+
The media library lets you upload images from the dashboard and insert them directly into posts.
105+
106+
By default, images are stored on disk in a `media/` directory next to `CONTENT_DIR` and served at `/media/{key}`. To use S3-compatible object storage instead (AWS S3, Cloudflare R2, NevaObjects, MinIO), set `MEDIA_STORAGE=s3` and the `S3_*` variables in `.env`:
107+
108+
```env
109+
MEDIA_STORAGE=s3
110+
S3_ENDPOINT=https://s3.nevaobjects.id
111+
S3_BUCKET=my-bucket
112+
S3_REGION=auto
113+
S3_ACCESS_KEY=your-access-key
114+
S3_SECRET_KEY=your-secret-key
115+
S3_PUBLIC_URL=https://s3.nevaobjects.id/my-bucket
116+
```
117+
118+
See [docs/configuration.md](docs/configuration.md) for provider-specific endpoint examples.
119+
120+
## Site Settings
121+
122+
Site metadata is stored in the database and can be updated from the dashboard at `/admin/settings` without restarting the server. The public `GET /api/settings` endpoint exposes these values to themes.
123+
124+
| Key | Description |
125+
|-----|-------------|
126+
| `site_name` | Display name of the site |
127+
| `site_description` | Short description used in meta tags |
128+
| `social_github` | GitHub profile or repo URL |
129+
| `social_twitter` | Twitter/X profile URL |
130+
| `social_linkedin` | LinkedIn profile URL |
131+
99132
## API
100133

101134
Full API reference: [docs/api.md](docs/api.md)

docs/api.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,128 @@ Get the current rebuild status.
335335
| `running` | Build in progress |
336336
| `success` | Last build succeeded |
337337
| `failed` | Last build failed; see `error` field |
338+
339+
---
340+
341+
### `GET /api/settings`
342+
343+
Get all site settings. Public endpoint — intended for themes to read site metadata.
344+
345+
**Response `200`**
346+
```json
347+
{
348+
"site_name": "My Blog",
349+
"site_description": "A personal blog about Go and systems.",
350+
"social_github": "https://github.com/example",
351+
"social_twitter": "https://twitter.com/example",
352+
"social_linkedin": ""
353+
}
354+
```
355+
356+
Returns a flat key/value object. Keys with no value set return an empty string.
357+
358+
---
359+
360+
### `GET /api/admin/settings`
361+
362+
Get all site settings (protected). Same response shape as `GET /api/settings`.
363+
364+
---
365+
366+
### `PUT /api/admin/settings`
367+
368+
Update one or more site settings (protected + CSRF).
369+
370+
**Request body**
371+
```json
372+
{
373+
"site_name": "My Blog",
374+
"site_description": "A personal blog about Go and systems.",
375+
"social_github": "https://github.com/example",
376+
"social_twitter": "",
377+
"social_linkedin": ""
378+
}
379+
```
380+
381+
Only known keys are accepted. Unknown keys return `400`.
382+
383+
| Key | Description |
384+
|-----|-------------|
385+
| `site_name` | Display name of the site |
386+
| `site_description` | Short description used in meta tags |
387+
| `social_github` | GitHub profile or repo URL |
388+
| `social_twitter` | Twitter/X profile URL |
389+
| `social_linkedin` | LinkedIn profile URL |
390+
391+
**Response `204`** No content.
392+
393+
**Errors**: `400` invalid JSON or unknown key
394+
395+
---
396+
397+
### `POST /api/admin/media`
398+
399+
Upload an image file (protected + CSRF). Accepts `multipart/form-data`.
400+
401+
**Form field**: `file` — the image to upload.
402+
403+
**Constraints**:
404+
- Max file size: 10 MB
405+
- Allowed types: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/svg+xml`
406+
- Content type is detected from file bytes, not the `Content-Type` header
407+
408+
**Response `201`**
409+
```json
410+
{
411+
"key": "550e8400-e29b-41d4-a716-446655440000-photo.jpg",
412+
"filename": "photo.jpg",
413+
"content_type": "image/jpeg",
414+
"size": 204800,
415+
"url": "https://example.com/media/550e8400-e29b-41d4-a716-446655440000-photo.jpg",
416+
"created_at": "2026-04-01T10:00:00Z"
417+
}
418+
```
419+
420+
**Errors**: `400` missing file or invalid form, `415` unsupported file type, `413` file too large
421+
422+
---
423+
424+
### `GET /api/admin/media`
425+
426+
List all uploaded files (protected + CSRF). Returns newest first.
427+
428+
**Response `200`**
429+
```json
430+
[
431+
{
432+
"key": "550e8400-e29b-41d4-a716-446655440000-photo.jpg",
433+
"filename": "photo.jpg",
434+
"content_type": "image/jpeg",
435+
"size": 204800,
436+
"url": "https://example.com/media/550e8400-e29b-41d4-a716-446655440000-photo.jpg",
437+
"created_at": "2026-04-01T10:00:00Z"
438+
}
439+
]
440+
```
441+
442+
Returns `[]` if no files have been uploaded.
443+
444+
---
445+
446+
### `DELETE /api/admin/media/{key}`
447+
448+
Delete an uploaded file by key (protected + CSRF). Removes the file from storage and the database record.
449+
450+
**Response `204`** No content.
451+
452+
**Errors**: `400` missing key, `500` storage or database error
453+
454+
---
455+
456+
## Static File Serving
457+
458+
### `GET /media/{key}`
459+
460+
Serves an uploaded file by key. Only available when `MEDIA_STORAGE=local` (default).
461+
When using S3-compatible storage, files are served directly from the bucket via the `url` field
462+
returned by the upload and list endpoints.

docs/configuration.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,67 @@ exposed. Consider unsetting them or using interactive setup instead.
6868

6969
---
7070

71+
## Webhook
72+
73+
| Variable | Default | Description |
74+
|----------|---------|-------------|
75+
| `WEBHOOK_SECRET` | _(empty)_ | Optional. If set, enables `POST /api/webhook/rebuild`. Pass the secret via `X-Webhook-Secret` header or `Authorization: Bearer`. |
76+
77+
---
78+
79+
## Media Library
80+
81+
| Variable | Default | Description |
82+
|----------|---------|-------------|
83+
| `SITE_URL` | `http://localhost:8090` | Base URL of the site. Used to build absolute URLs for locally stored media files. |
84+
| `MEDIA_STORAGE` | `local` | Storage backend. `local` stores files on disk; `s3` uses any S3-compatible provider. |
85+
86+
When `MEDIA_STORAGE=local`, uploaded files are stored in a `media/` directory next to `CONTENT_DIR`
87+
and served at `GET /media/{key}`.
88+
89+
---
90+
91+
## S3-Compatible Storage
92+
93+
Required when `MEDIA_STORAGE=s3`. Supports AWS S3, Cloudflare R2, NevaObjects, MinIO, and any
94+
S3-compatible provider.
95+
96+
| Variable | Default | Description |
97+
|----------|---------|-------------|
98+
| `S3_ENDPOINT` | _(required)_ | API endpoint of the S3 provider. |
99+
| `S3_BUCKET` | _(required)_ | Bucket name. |
100+
| `S3_REGION` | `auto` | Region. Use `auto` for providers that don't require one (R2, NevaObjects). |
101+
| `S3_ACCESS_KEY` | _(required)_ | Access key ID. |
102+
| `S3_SECRET_KEY` | _(required)_ | Secret access key. |
103+
| `S3_PUBLIC_URL` | _(required)_ | Base URL prepended to file keys for public access. |
104+
105+
Provider-specific endpoint and public URL examples:
106+
107+
| Provider | `S3_ENDPOINT` | `S3_PUBLIC_URL` |
108+
|----------|--------------|-----------------|
109+
| AWS S3 | `https://s3.<region>.amazonaws.com` | `https://<bucket>.s3.<region>.amazonaws.com` |
110+
| Cloudflare R2 | `https://<account_id>.r2.cloudflarestorage.com` | your custom domain or R2 public URL |
111+
| NevaObjects | `https://s3.nevaobjects.id` | `https://s3.nevaobjects.id/<bucket>` |
112+
| MinIO | `https://minio.example.com` | `https://minio.example.com/<bucket>` |
113+
114+
---
115+
116+
## SMTP (Newsletter)
117+
118+
Required to send newsletters via `POST /api/admin/newsletter/send`.
119+
120+
| Variable | Default | Description |
121+
|----------|---------|-------------|
122+
| `SMTP_HOST` | _(empty)_ | SMTP server hostname, e.g. `smtp.mailgun.org` |
123+
| `SMTP_PORT` | `587` | SMTP port |
124+
| `SMTP_USERNAME` | _(empty)_ | SMTP username |
125+
| `SMTP_PASSWORD` | _(empty)_ | SMTP password |
126+
| `SMTP_FROM` | _(empty)_ | Sender address, e.g. `newsletter@example.com` |
127+
128+
If `SMTP_HOST` is not set, the newsletter send endpoint returns `503`.
129+
130+
---
131+
71132
## Example `.env`
72133

73134
```env
@@ -80,6 +141,26 @@ JWT_SECRET=your-generated-secret-here
80141
THEME_DIR=theme
81142
THEME_BUILD_CMD=npm run build
82143
THEME_SERVICE=
144+
145+
# Media (local storage, default)
146+
SITE_URL=https://example.com
147+
MEDIA_STORAGE=local
148+
149+
# Media (S3-compatible storage, uncomment to use)
150+
# MEDIA_STORAGE=s3
151+
# S3_ENDPOINT=https://s3.nevaobjects.id
152+
# S3_BUCKET=my-bucket
153+
# S3_REGION=auto
154+
# S3_ACCESS_KEY=
155+
# S3_SECRET_KEY=
156+
# S3_PUBLIC_URL=https://s3.nevaobjects.id/my-bucket
157+
158+
# Newsletter (optional)
159+
# SMTP_HOST=smtp.mailgun.org
160+
# SMTP_PORT=587
161+
# SMTP_USERNAME=
162+
# SMTP_PASSWORD=
163+
# SMTP_FROM=newsletter@example.com
83164
```
84165

85166
---

0 commit comments

Comments
 (0)