Files
classroomio/apps/docs/content/docs/guides/contributing/org-site-opengraph-and-favicon.mdx
T
Abdulmuizz HamzatandRotimi Best b599f1a017 chore(docs): migrate the docs site from Fumadocs to Blume (#753)
* Update documentation for ClassroomIO: change Fumadocs to Blume, revise MDX format conventions, and clarify sidebar navigation requirements.

* docs: update README and scripts for improved preview and error handling

* docs: update Discord link in configuration and documentation

* docs: add token-auth route and update documentation formatting

* Fix formatting in auto-comment-issues.yml

* Fix JSON syntax by adding a comma

* fix(docs): declare Blume's Astro build deps so Cloudflare build resolves them

blume build generates an Astro project under apps/docs/.blume/ that imports
astro, @astrojs/mdx, @tailwindcss/vite, tailwindcss, @tailwindcss/typography,
@shikijs/twoslash and @scalar/astro. Under pnpm's isolated node_modules these
are nested inside blume and not resolvable from the docs app, so a clean
--frozen-lockfile build (Cloudflare) fails at 'Cannot find module astro/config'.
Declare them directly in @cio/docs, pinned to blume's resolved versions.

* refactor(docs): update API reference paths and documentation for clarity

* fix: change platofrm url to docs home page

* Refactor documentation for self-hosting ClassroomIO

- Removed the Supabase Cloud setup guide as it is no longer needed.
- Updated the Enterprise SSO documentation to include a reference link for SSO & licensing.
- Added new documentation for Content Security Policy (CSP) configuration.
- Created a new section for configuring SMTP for email notifications.
- Introduced a comprehensive configuration index for self-hosted ClassroomIO, detailing environment variables and their usage.
- Added documentation for optional integrations including Google OAuth, OpenAI, and Unsplash.
- Documented the generation and usage of required secrets for ClassroomIO.
- Created a section for SSO & licensing, detailing the requirements for Enterprise features.
- Added object storage configuration documentation, covering both bundled MinIO and external S3-compatible services.
- Documented versioning and upgrade processes for ClassroomIO.
- Updated the Coolify and Docker self-hosting guides to streamline configuration references and upgrade instructions.
- Enhanced the Railway self-hosting guide with links to relevant configuration sections.
- Updated token authentication documentation to reference SSO & licensing.

* Add comprehensive guides for ClassroomIO features and functionalities

- Introduced a new guide for the Student Dashboard, detailing navigation and features available to students.
- Added a guide on using Tags for organizing and filtering courses within ClassroomIO.
- Created a guide on how to take attendance in courses, including marking attendance and related functionalities.
- Developed a Terminology guide to clarify key terms used throughout ClassroomIO.
- Implemented a Token Auth guide for signing users into ClassroomIO from external applications using JWT.
- Added instructions for using math in the editor with LaTeX support.
- Created a guide for customizing the welcome email sent to students upon enrollment in courses.
- Added a guide explaining what ClassroomIO is and its key features.
- Updated the home documentation to provide a clearer overview of ClassroomIO and its resources.
- Enhanced self-hosted configuration documentation with a new guide for deploying ClassroomIO on Dokploy.
- Updated links in the self-hosted configuration documentation for better navigation.
- Added new images to support the Dokploy deployment guide.
- Updated asset packaging script to handle redirects for the new guide structure.
- Fixed footer link to point to the correct Getting Started guide.

* chore: uprage version

* feat(docs): add DigitalOcean deployment guide and flatten guides structure

- Add a guide for deploying ClassroomIO on DigitalOcean using Docker Compose
  and Caddy for automatic HTTPS.
- List DigitalOcean as a deployment option in the self-hosting index.
- Move guides out of content/docs/guides into content/docs (student-dashboard,
  tags, terminology, token-auth, what-is-classroomio, audience, cohorts,
  community, course, enterprise-sso, mcp, organization, quickstart/*).
- Rename home.mdx to index.mdx.
- Update package-assets script to emit redirects for the old guides paths.

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-22 22:30:10 +01:00

198 lines
9.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Org-Site Open Graph & Favicon
description: Contributor reference for dynamic OG image generation, favicon fallback, CDN caching, and social embed behavior on public org-site routes.
---
This document describes how ClassroomIO generates **Open Graph preview images** and **favicons** for public org-site routes (`*.myclassroomio.com`, BYOD custom domains, and self-hosted single-org mode). It covers caching layers, invalidation, and why crawlers such as Discord need a fast, stable image URL.
For admin-facing behavior, see [Sharing Your Academy (Open Graph & Favicon)](/guides/academy-sharing-and-branding).
## Overview
```mermaid
flowchart LR
subgraph dashboard [Dashboard SSR]
Layout["+layout.server.ts"]
Meta["metaTags.server.ts"]
Favicon["org-site-favicon.svelte"]
end
subgraph api [API]
OgRoute["GET /org-site/og/:siteName.png"]
OgService["services/org/og.ts"]
OgCache["services/org/og-cache.ts"]
CFRender["Cloudflare Browser Rendering"]
end
subgraph storage [Storage & edge]
R2["R2 media bucket og/{siteName}.png"]
CDN["img.cdn.clsrio.com"]
Worker["tenant-router edge cache"]
end
Layout -->|warm fetch| OgRoute
Meta -->|og:image URL| CDN
Favicon -->|icon href| CDN
OgRoute --> OgService
OgService --> OgCache
OgCache -->|cache hit| R2
OgService -->|cache miss| CFRender
CFRender --> OgCache
OgCache -->|persist| R2
R2 --> CDN
OgRoute --> Worker
```
| Concern | Location |
| --- | --- |
| OG HTML template | `apps/api/src/utils/org-site-og.ts` |
| OG generation service | `apps/api/src/services/org/og.ts` |
| R2 read/write cache | `apps/api/src/services/org/og-cache.ts` |
| API route | `apps/api/src/routes/org-site/og.ts` → mounted at `/org-site/og` in `apps/api/src/app.ts` |
| Meta tags (SSR) | `apps/dashboard/src/lib/utils/functions/metaTags.server.ts` |
| OG public URL resolution | `apps/dashboard/src/lib/utils/functions/org-site-og-url.ts` |
| Shared URL helpers | `packages/utils/src/org-site/og-public-url.ts` |
| Favicon component | `apps/dashboard/src/lib/features/app/org-site-favicon.svelte` |
| Favicon href helper | `apps/dashboard/src/lib/utils/functions/org-branding.ts` |
| OG warm on page load | `apps/dashboard/src/routes/+layout.server.ts` |
| Proxy / edge cache | `apps/tenant-router/src/index.ts` |
| Plan gating (watermark, tagline) | `packages/utils/src/plans/org-plan.ts` (`isOrgOnFreePlan`) |
## Open Graph image generation
### Endpoint
```
GET /org-site/og/:siteName.png
```
The `.png` suffix is optional; `normalizeOrgOgSiteName()` strips it before lookup.
The handler returns `image/png` with `ETag`, `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`, and falls back to `https://brand.cdn.clsrio.com/og/classroomio-opengraph.jpg` on 404 or render errors.
### Render pipeline
1. Load org by `siteName` from the database.
2. Resolve plan via `getActiveOrganizationPlan` + `isOrgOnFreePlan`.
3. Build render input:
- `orgName`, `logoUrl` (avatar), `themeColor`, `showWatermark` (free plan)
- `tagline` — **paid plans only**; from `extractOrgTagline(landingpage)` (footer brand tagline, else hero subheading)
4. Compute a **content etag** (16-char SHA-256 prefix) from `{ siteName, orgName, logoUrl, tagline, themeColor, showWatermark }`.
5. Check R2 cache (see below). On miss, render HTML via `renderOrgSiteOg()` and screenshot with **Cloudflare Browser Rendering** (`getCloudflarePngBuffer`, viewport 1200×630).
6. Persist PNG to R2 and return bytes.
### Template behavior
- **Logo present:** image fills the logo container; no theme-color background behind the logo (avoids washing out light logos).
- **No logo:** initials fallback on a theme-colored rounded square.
- **Free plan:** no tagline on image; **Powered by ClassroomIO** watermark.
- **Paid plan:** tagline under org name; no watermark.
## Caching
OG images pass through several cache layers. Understanding them explains stale Discord previews and regeneration timing.
### 1. R2 object cache (source of truth for CDN URL)
| Property | Value |
| --- | --- |
| **Bucket** | Media bucket (`CLOUDFLARE_IMAGE_BUCKET_DOMAIN` / `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`) |
| **Key** | `og/{siteName}.png` |
| **Invalidation** | Object metadata field `og-etag` compared to computed content etag; mismatch → regenerate and overwrite |
| **Object Cache-Control** | `public, max-age=3600, stale-while-revalidate=86400` |
The public URL served in meta tags (cloud default):
```
https://img.cdn.clsrio.com/og/{siteName}.png
```
Same path on every request — no query-string cache bust. Updates rely on overwrite + CDN TTL expiry.
### 2. API in-memory / response cache
The API route sets `ETag` and honors `If-None-Match` (304). Rendered bytes may be served from R2 without calling Cloudflare Browser Rendering when the content etag matches.
**Cold generation** (Cloudflare screenshot) can take several seconds. Social crawlers (Discord ~3–5s timeout) fail if the first fetch is too slow — this motivated CDN-backed URLs and warming.
### 3. Dashboard OG warm
On every org-site `+layout.server.ts` load, a **fire-and-forget** `fetch()` hits the internal OG API URL (`resolveOrgSiteOgWarmUrl`) so the PNG is generated and written to R2 before crawlers request the CDN object.
This is best-effort (not awaited). It does **not** run at org-save time.
### 4. Tenant-router edge cache (cloud)
For `*.myclassroomio.com`, requests to `/proxy/org-site/og/*` are proxied to the API. The worker applies:
- `cf.cacheEverything: true`, `cacheTtl: 3600`
- Response `Cache-Control: public, max-age=3600, stale-while-revalidate=86400`
Meta tags in cloud production prefer the **CDN URL**, not `/proxy/…`, so crawlers usually skip this hop. The edge cache remains a fallback for legacy or self-hosted proxy paths.
### 5. Crawler / chat app caches
Discord, Slack, iMessage, etc. cache **embed metadata** and **images** independently of ClassroomIO. Failed embeds may be cached for minutes. Users must often **re-post** a link to see updates even after R2 and CDN have fresh content.
## Meta tag resolution
`getBaseMetaTags()` in `metaTags.server.ts` sets title, description, and `og:image` for org sites.
**OG image URL priority** (`resolveOrgSiteOgImageUrl`):
1. `PUBLIC_OG_IMAGE_URL` — global override (self-hosted branding)
2. **Cloud CDN** — `PUBLIC_MEDIA_CDN_URL` or default `https://img.cdn.clsrio.com` → `/og/{siteName}.png`, only when a `HEAD` request confirms the object exists
3. `PUBLIC_SERVER_URL` — direct API URL `/org-site/og/{siteName}.png` (used until the CDN object is available)
4. Tenant origin — `{origin}/proxy/org-site/og/{siteName}.png`
Title and description come from `org-site-meta.ts` (hero heading/subheading, org name). These update on every SSR response and do not depend on the PNG cache.
## Favicon
### Scope
Rendered from root `+layout.svelte` via `OrgSiteFavicon` when `data.isOrgSite` is true. Default ClassroomIO favicons in `app.html` are omitted for org sites; non-org routes inject `/favicon.ico` and `/logo-32.png` from the root layout.
### Resolution (`getOrgFaviconHref`)
1. Custom `org.favicon` if set (uploaded under **Settings → Organization → Custom Domain**)
2. Else `org.avatarUrl`
3. Relative URLs resolved against `page.url.origin`
## Invalidation matrix
| Org field changed | Content etag changes? | Meta text updates | PNG regen |
| --- | --- | --- | --- |
| `name` | Yes | Yes (title) | Yes |
| `theme` | Yes | No | Yes |
| `avatarUrl` | Yes | No | Yes |
| Landing tagline / hero subheading | Yes (paid tagline on image) | Yes (description) | Yes (paid only on image) |
| Plan (free ↔ paid) | Yes (`showWatermark`, tagline) | No | Yes |
| `siteName` | N/A (different object key) | N/A | New key `og/{newSiteName}.png` |
Regeneration runs on the next OG API request or warm fetch after the etag mismatch.
:::info[No save-hook regen]
There is no background job on organization `PUT`. Regen depends on a page visit (warm) or an external fetch of the OG endpoint.
:::
## Self-hosted notes
- Set `PUBLIC_IS_SELFHOSTED=true` in both dashboard and API for consistent behavior.
- Without `CLOUDFLARE_IMAGE_BUCKET_DOMAIN` / `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`, OG images are not persisted to R2; meta tags fall back to `PUBLIC_SERVER_URL` or `/proxy/…`.
- Cloudflare Browser Rendering credentials (`CLOUDFLARE_RENDERING_API_KEY`, account ID) are required for PNG generation.
- Optional: `PUBLIC_MEDIA_CDN_URL` on the dashboard to override the CDN base for `og:image`.
- BYOD custom domains hit the dashboard Render service directly (not tenant-router); `/proxy/*` forwarding in `hooks.server.ts` mirrors tenant-router behavior.
## Local development
- Org sites locally: `http://localhost:5173/?org={siteName}` (sets `_orgSiteName` cookie).
- OG endpoint: `http://localhost:3002/org-site/og/{siteName}.png` or via dashboard proxy `http://localhost:5173/proxy/org-site/og/{siteName}.png`.
- CDN URLs point at production `img.cdn.clsrio.com` when not self-hosted; local previews use proxy/API URLs unless you override env vars.
## Related docs
- [Sharing Your Academy (Open Graph & Favicon)](/guides/academy-sharing-and-branding) — admin-facing guide
- [Cloudflare Setup](/guides/contributing/cloudflare-setup) — R2 media bucket and `CLOUDFLARE_IMAGE_BUCKET_DOMAIN`
- [Customize Your Organization](/guides/customize-organization) — logo, name, theme (user docs)