Sites
Sites are the top-level organizational unit in Forja. All content, media, navigation, and configuration are scoped to a site.
Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /sites | Read | List all sites (filtered by membership or API key scope) |
| POST | /sites | Admin (API key) / Any (Clerk) | Create a new site |
| GET | /sites/{id} | Read | Get a site by ID |
| GET | /sites/by-slug/{slug} | Read | Get a site by slug |
| PUT | /sites/{id} | Admin | Update a site |
| DELETE | /sites/{id} | Owner | Soft delete a site |
| GET | /sites/{slug}/sitemap.xml | Public | XML sitemap of all published content |
| GET | /sites/{slug}/robots.txt | Public | Rendered robots.txt from site rules |
| GET | /sites/{slug}/site.webmanifest | Public | Web app manifest (JSON) |
| GET | /sites/{slug}/browserconfig.xml | Public | Browserconfig for IE/Edge tiles |
| POST | /sites/{site_id}/favicon | Admin | Upload source image and generate favicon package |
| GET | /sites/{site_id}/favicon | Read | Get favicon variant URLs and HTML snippet |
| GET | /sites/{id}/context | Read | Get site context (features, modules, suggestions) |
| GET | /sites/{site_id}/settings | Admin | Get site settings |
| PUT | /sites/{site_id}/settings | Admin | Update site settings |
| GET | /sites/{site_id}/locales | Read | List site locales |
| POST | /sites/{site_id}/locales | Admin | Add a locale to a site |
| PUT | /sites/{site_id}/locales/{locale_id} | Admin | Update a site locale |
| DELETE | /sites/{site_id}/locales/{locale_id} | Admin | Remove a locale from a site |
| PUT | /sites/{site_id}/locales/{locale_id}/default | Admin | Set the default locale |
| GET | /sites/{site_id}/onboarding | Read | Get onboarding progress |
| POST | /sites/{site_id}/onboarding/{step} | Author | Complete an onboarding step |
| GET | /my/memberships | Clerk JWT | Get current user's site memberships |
List Sites
Returns sites visible to the authenticated user. Clerk users see sites they have memberships for (system admins see all). API key users see sites matching their key scope.
curl -H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites
Response 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Portfolio",
"slug": "my-portfolio",
"description": "Personal developer portfolio",
"base_url": "https://portfolio.example.com",
"is_active": true,
"created_at": "2025-01-15T12:00:00Z",
"updated_at": "2025-01-15T12:00:00Z"
}
]
Create a Site
Clerk-authenticated users automatically become the site owner. API keys require Admin+ permission and must not be site-scoped.
You can optionally include locales in the creation request to set up site locales in a single call.
curl -X POST \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{
"name": "My Blog",
"slug": "my-blog",
"description": "A personal blog",
"base_url": "https://blog.example.com"
}' \
https://your-domain.com/api/v1/sites
Response 201 Created
Get a Site by Slug
curl -H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/by-slug/my-blog
Update a Site
Requires Admin role on the site.
curl -X PUT \
-H "X-API-Key: oy_live_abc123..." \
-H "Content-Type: application/json" \
-d '{"name": "Updated Name", "base_url": "https://new-domain.example.com"}' \
https://your-domain.com/api/v1/sites/{id}
All fields are optional. The base_url field must be a valid URL with protocol (e.g. https://example.com). It is used for sitemap generation, canonical/OG meta tags, and preview links. When null, SEO features that require a canonical URL are gracefully omitted.
Delete a Site
Soft deletes the site. Requires Owner role.
curl -X DELETE \
-H "Authorization: Bearer eyJ..." \
https://your-domain.com/api/v1/sites/{id}
Response 204 No Content
Get Sitemap
Returns an XML sitemap of all published content for a site. This is a public endpoint (no authentication required) intended for search engine consumption.
The site must have a base_url configured. If not set, the endpoint returns 404 with the message "Set a Site URL before generating a sitemap."
curl https://your-domain.com/api/v1/sites/my-blog/sitemap.xml
Response 200 OK (Content-Type: application/xml, Cache-Control: public, max-age=3600)
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://example.com/about</loc>
<lastmod>2025-03-01T12:00:00+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/about"/>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/about"/>
</url>
<url>
<loc>https://example.com/blog/hello-world</loc>
<lastmod>2025-06-15T10:30:00+00:00</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
</urlset>
Included content:
- Pages (priority 0.8, changefreq monthly)
- Blog posts (priority 0.6, changefreq weekly)
- Legal documents (priority 0.4, changefreq yearly)
Only published content is included -- drafts, archived, and deleted content are excluded. Multi-locale alternate links are added when the site has multiple active locales and the content has translations.
Get robots.txt
Returns the rendered robots.txt for a site. Rules are configured via site settings. This is a public endpoint (no authentication required).
When a base_url is configured, a Sitemap: directive is automatically appended.
curl https://your-domain.com/api/v1/sites/my-blog/robots.txt
Response 200 OK (Content-Type: text/plain, Cache-Control: public, max-age=3600)
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
Rules are stored as structured JSON in site settings (robots_txt_rules) and rendered to standard robots.txt format at response time.
Get Web Manifest
Returns a site.webmanifest JSON document for PWA / Android Chrome. This is a public endpoint.
curl https://your-domain.com/api/v1/sites/my-blog/site.webmanifest
Response 200 OK (Content-Type: application/manifest+json, Cache-Control: public, max-age=3600)
{
"name": "My Blog",
"short_name": "My Blog",
"icons": [
{ "src": "https://cdn.example.com/site_favicons/.../android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "https://cdn.example.com/site_favicons/.../android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#4a90d9",
"background_color": "#ffffff",
"display": "standalone"
}
The theme_color and background_color values come from site settings. Icons are populated after uploading a favicon source image.
Get Browserconfig
Returns a browserconfig.xml for IE/Edge tile configuration. This is a public endpoint.
curl https://your-domain.com/api/v1/sites/my-blog/browserconfig.xml
Response 200 OK (Content-Type: application/xml, Cache-Control: public, max-age=3600)
Upload Favicon
Upload a source image (512x512px+ recommended) to generate a complete favicon package. Requires Admin role. Accepts PNG, JPEG, GIF, or WebP (max 10 MB).
Generated variants:
favicon.ico(multi-resolution: 16x16, 32x32, 48x48)favicon-16x16.png,favicon-32x32.pngapple-touch-icon.png(180x180)android-chrome-192x192.png,android-chrome-512x512.png
Icons are stored under site_favicons/<site_id>/ and do not appear in the media library. The site's favicon_url is automatically updated to point to the 32x32 PNG for backwards compatibility.
curl -X POST \
-H "Authorization: Bearer eyJ..." \
-F "file=@icon-512.png" \
https://your-domain.com/api/v1/sites/{site_id}/favicon
Response 200 OK
{
"variants": [
{ "name": "favicon.ico", "url": "https://...", "width": 48, "height": 48 },
{ "name": "favicon-16x16.png", "url": "https://...", "width": 16, "height": 16 },
{ "name": "favicon-32x32.png", "url": "https://...", "width": 32, "height": 32 },
{ "name": "apple-touch-icon.png", "url": "https://...", "width": 180, "height": 180 },
{ "name": "android-chrome-192x192.png", "url": "https://...", "width": 192, "height": 192 },
{ "name": "android-chrome-512x512.png", "url": "https://...", "width": 512, "height": 512 }
],
"head_snippet": "<link rel=\"icon\" type=\"image/x-icon\" href=\"...\">..."
}
Get Favicon
Returns the current favicon variant URLs and an HTML <head> snippet. Returns 404 if no favicon package has been generated yet.
curl -H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/{site_id}/favicon
Response 200 OK — Same shape as the upload response.
Get Site Context
Returns contextual information about a site for adaptive UI, including enabled features, content modules, and UI suggestions. This drives progressive disclosure in the admin dashboard.
curl -H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/{id}/context
Response 200 OK
{
"member_count": 1,
"current_user_role": "owner",
"features": {
"editorial_workflow": false,
"scheduling": true,
"versioning": true,
"analytics": false
},
"suggestions": {
"show_team_workflow_prompt": false
},
"modules": {
"blog": true,
"pages": true,
"portfolio": false,
"legal": false,
"documents": false,
"ai": false
}
}
Get Site Settings
Returns all effective settings for a site (database values merged with defaults). Requires Admin role.
curl -H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/{site_id}/settings
Response 200 OK
{
"max_document_file_size": 10485760,
"max_media_file_size": 52428800,
"analytics_enabled": false,
"maintenance_mode": false,
"contact_email": "",
"editorial_workflow_enabled": false,
"preview_templates": [],
"module_blog_enabled": true,
"module_pages_enabled": true,
"module_portfolio_enabled": false,
"module_legal_enabled": false,
"module_documents_enabled": false,
"module_ai_enabled": false,
"robots_txt_rules": [{"user_agent": "*", "rules": [{"directive": "Allow", "path": "/"}]}],
"seo_title_template": "{{title}} | {{site_name}}",
"seo_default_description": "",
"seo_default_og_image_id": null,
"theme_color": "#ffffff",
"background_color": "#ffffff",
"code_injection_head": "",
"code_injection_footer": ""
}
Update Site Settings
Updates site settings. All fields are optional -- only provided fields are changed. Requires Admin role.
curl -X PUT \
-H "X-API-Key: oy_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"analytics_enabled": true,
"editorial_workflow_enabled": true,
"contact_email": "admin@example.com"
}' \
https://your-domain.com/api/v1/sites/{site_id}/settings
Response 200 OK -- Returns the full updated SiteSettingsResponse.
List Site Locales
Returns all locales assigned to a site, with full locale details (code, name, direction).
curl -H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/{site_id}/locales
Response 200 OK
[
{
"site_id": "550e8400-e29b-41d4-a716-446655440000",
"locale_id": "660e8400-e29b-41d4-a716-446655440000",
"is_default": true,
"is_active": true,
"url_prefix": "en",
"created_at": "2025-01-15T12:00:00Z",
"code": "en",
"name": "English",
"native_name": "English",
"direction": "ltr"
}
]
Add Locale to Site
Assigns a locale to a site. Requires Admin role.
curl -X POST \
-H "X-API-Key: oy_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"locale_id": "660e8400-e29b-41d4-a716-446655440000",
"is_default": false,
"url_prefix": "de"
}' \
https://your-domain.com/api/v1/sites/{site_id}/locales
Response 201 Created
Update Site Locale
Updates properties of a site locale assignment. All fields are optional. Requires Admin role.
curl -X PUT \
-H "X-API-Key: oy_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"is_active": true,
"url_prefix": "de"
}' \
https://your-domain.com/api/v1/sites/{site_id}/locales/{locale_id}
Response 200 OK
Remove Locale from Site
Removes a locale assignment from a site. Requires Admin role.
curl -X DELETE \
-H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/{site_id}/locales/{locale_id}
Response 204 No Content
Set Default Locale
Sets a locale as the site's default. Requires Admin role.
curl -X PUT \
-H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/{site_id}/locales/{locale_id}/default
Response 200 OK
Get Onboarding Progress
Returns the onboarding checklist progress for the current user on a site, including completed steps and overall percentage.
curl -H "X-API-Key: oy_live_abc123..." \
https://your-domain.com/api/v1/sites/{site_id}/onboarding
Response 200 OK
{
"completed_steps": [
{
"step_key": "edit_first_post",
"completed_at": "2025-01-15T10:30:00Z"
}
],
"total_steps": 5,
"completed_count": 1,
"progress_percent": 20
}
Complete Onboarding Step
Marks an onboarding step as completed for the current user on a site. Idempotent -- completing an already-completed step is a no-op.
curl -X POST \
-H "X-API-Key: oy_live_abc123..." \
-H "Content-Type: application/json" \
-d '{"step_key": "edit_first_post"}' \
https://your-domain.com/api/v1/sites/{site_id}/onboarding/{step}
Response 200 OK
Get My Memberships
Returns all site memberships for the currently authenticated Clerk user. Only available for Clerk JWT authentication (not API keys).
curl -H "Authorization: Bearer eyJ..." \
https://your-domain.com/api/v1/my/memberships
Response 200 OK
[
{
"site_id": "550e8400-e29b-41d4-a716-446655440000",
"site_name": "My Portfolio",
"site_slug": "my-portfolio",
"role": "owner"
}
]