Skip to main content

Sites

Sites are the top-level organizational unit in Forja. All content, media, navigation, and configuration are scoped to a site.

Endpoints

MethodPathPermissionDescription
GET/sitesReadList all sites (filtered by membership or API key scope)
POST/sitesAdmin (API key) / Any (Clerk)Create a new site
GET/sites/{id}ReadGet a site by ID
GET/sites/by-slug/{slug}ReadGet a site by slug
PUT/sites/{id}AdminUpdate a site
DELETE/sites/{id}OwnerSoft delete a site
GET/sites/{slug}/sitemap.xmlPublicXML sitemap of all published content
GET/sites/{slug}/robots.txtPublicRendered robots.txt from site rules
GET/sites/{slug}/site.webmanifestPublicWeb app manifest (JSON)
GET/sites/{slug}/browserconfig.xmlPublicBrowserconfig for IE/Edge tiles
POST/sites/{site_id}/faviconAdminUpload source image and generate favicon package
GET/sites/{site_id}/faviconReadGet favicon variant URLs and HTML snippet
GET/sites/{id}/contextReadGet site context (features, modules, suggestions)
GET/sites/{site_id}/settingsAdminGet site settings
PUT/sites/{site_id}/settingsAdminUpdate site settings
GET/sites/{site_id}/localesReadList site locales
POST/sites/{site_id}/localesAdminAdd a locale to a site
PUT/sites/{site_id}/locales/{locale_id}AdminUpdate a site locale
DELETE/sites/{site_id}/locales/{locale_id}AdminRemove a locale from a site
PUT/sites/{site_id}/locales/{locale_id}/defaultAdminSet the default locale
GET/sites/{site_id}/onboardingReadGet onboarding progress
POST/sites/{site_id}/onboarding/{step}AuthorComplete an onboarding step
GET/my/membershipsClerk JWTGet 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.png
  • apple-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"
}
]