First-party frontend

Roosty’s first-party frontend uses Leptos 0.8 and Tailwind CSS 4. The existing Axum process renders complete HTML on the server, serves the generated CSS, JavaScript, and WebAssembly assets, and exposes narrowly scoped server functions for hydrated interactions. Caddy remains a reverse proxy and does not own frontend routing.

Component system

The first-party UI uses daisyUI 5 components and standard Tailwind utilities. Roosty overrides only the colors of daisyUI’s light and dark themes, retaining its green accent and neutral surfaces with automatic system color-scheme behavior. Typography, radii, sizing, borders, depth, motion, and component behavior use framework defaults. Reusable Leptos components own repeated navigation, account menus, cards, headings, administrator panels, fields, notices, and confirmation controls, while native links, buttons, inputs, forms, tables, and details elements retain their browser semantics.

New controls and layouts should use daisyUI component classes and standard Tailwind utilities instead of authored CSS selectors. The stylesheet is reserved for loading Tailwind and daisyUI and declaring the two Roosty color palettes. The daisyUI standalone build plugins are pinned and vendored below crates/web-ui/style/vendor because Cargo Leptos uses Tailwind’s standalone executable. Update both plugin files, their checksums, and the versioned license link together when upgrading daisyUI.

Route ownership

UI routes are registered explicitly from the Leptos route list. In addition to / and /about, local accounts have /@username, /@username/with_replies, /@username/media, and /@username/tagged/hashtag profile timelines. Local status permalinks use /@username/status-id. Direct requests and browser refreshes therefore use the same server renderer as client navigation. Generated assets live below /pkg, and internal UI server functions live below /api/web. Roosty deliberately has no global single-page-app fallback so missing Mastodon API and ActivityPub routes keep their existing response semantics.

The browser routes are separate from the JSON-only ActivityPub actor and Note identifiers at /users/username and /users/username/statuses/status-id; those protocol routes do not use content negotiation.

Rendering and SEO

SEO-relevant content must be present in the initial response. Each public route owns its title, description, canonical URL, and Open Graph metadata through Leptos Meta. Route data should use a blocking SSR resource when it affects visible content or metadata; hydration reuses the serialized resource result instead of replacing server-rendered content after startup.

Profile pages include profile metadata, fields, counts, featured tags, pins, and 20-post UUID-cursor pages. “Load more” appends and deduplicates posts after hydration and replaces the cursor query in browser history. Profile roots omit replies; the replies, media, and tagged tabs apply their named filters. Hydrated tab navigation preserves the profile identity card and tab bar while only the selected timeline loads. Status pages render the visible bounded mixed local/remote thread, content warnings, sensitive media, accessible descriptions, polls, quotes, and preview cards.

Anonymous documents expose public and unlisted posts. A signed-in local account can additionally see follower-only and direct posts permitted by its follow, recipient, conversation, ownership, and block state. These reads use a read-only database snapshot. Session-aware documents and loaders are private, uncached, and vary on the session cookie. Missing, deleted, suspended, inaccessible, and author-mismatched resources all return the same not-found response.

Profile and article documents include canonical URLs without cursor queries, Open Graph data, ActivityPub alternate links, fediverse:creator, and Mastodon-compatible h-card/h-entry microformats. Private pages, later cursor pages, limited accounts, and non-discoverable profiles are marked noindex. Sensitive status metadata contains only the content warning and never a preview image.

When global search indexing is enabled, eligible profile roots include typed ProfilePage JSON-LD and eligible status permalinks include Google-supported SocialMediaPosting JSON-LD with authors, interaction counters, media, edits, and safe visible replies. Sensitive posts never receive JSON-LD or sitemap promotion. Media-only posts receive posting markup only when an image or a complete video representation is available. /robots.txt advertises /sitemap.xml, whose cursor-addressed profile and post sitemap chunks contain at most 50,000 canonical URLs each. Profiles must be active, discoverable, and unlimited; promoted posts must additionally be public and non-sensitive.

After deployment, validate representative profile, text, image, video, edited, and reply-thread URLs with Google’s Rich Results Test. Submit /sitemap.xml in Search Console, inspect live URLs, and request recrawling where appropriate. Search indexing and rich-result display remain at Google’s discretion.

The welcome and about pages present the operator-configured instance name and description rather than Roosty project marketing. A missing or blank description uses neutral social-web copy. Roosty appears as software attribution with its release version in the shared footer.

The shared roosty-web-ui crate is compiled once with ssr for the native server and once with hydrate for wasm32-unknown-unknown. Server-only database and authentication services cross that boundary through UiBackend; they are never compiled into or exposed to the browser bundle.

Authentication

The first-party UI uses Roosty’s signed, secure, HTTP-only session cookie. It is not registered as an OAuth client and does not store bearer tokens in browser storage. A read-only bootstrap server function projects only public instance metadata and a small optional account summary. Invalid, expired, and deleted-account sessions render as anonymous.

Leptos renders the /login, /auth/edit, and OAuth authorization views, while the existing server handlers remain authoritative for credential verification, session cookies, password updates, and OAuth grants. Native HTML form submissions use POST/Redirect/GET; fixed Strum-backed values select user-facing results and OAuth consent decisions without putting credentials or arbitrary errors in URLs. Login return paths remain sanitized and same-origin. The password page and OAuth consent flow redirect anonymous visitors through login.

Links to server-owned account routes use rel="external" so Leptos performs a full document navigation instead of resolving them as client-side UI routes. Future state-changing UI server functions must validate both the session and a CSRF token.

Administration

Administrators have an SSR and hydrated operations interface with a responsive category menu: work queue at /admin, local accounts at /admin/accounts, remote accounts at /admin/remote-accounts, and audit log at /admin/audit-log. The navigation link is projected only for accounts whose persisted is_admin flag is set, and the server independently protects every admin route. Anonymous requests return through login; authenticated non-administrators receive a forbidden response.

Each category has a separate JSON GET loader and queries only the data it presents. The active category refetches every 15 seconds while the document is visible, and its Refresh button triggers the same in-place request without a page reload. These responses are marked Cache-Control: no-store. Queue data exposes only job kind, lifecycle state, attempts, scheduling timestamps, and bounded error text. Raw job payloads are never sent to the UI. Process-local Prometheus metrics remain infrastructure endpoints.

Account creation, password reset, and local or cached-remote account limits use native forms with a session-bound HMAC CSRF token. Sensitive account actions open a daisyUI confirmation modal before the native form is submitted. Mutations and their audit records commit in one transaction. Generated temporary passwords are returned only by the successful response and are not stored in audit metadata.

OAuth views

OAuth consent and out-of-band code results use SSR-only Leptos documents with the shared instance chrome and stylesheet. They intentionally use native forms without hydration: consent needs no browser-side state, and an out-of-band one-time code must stay in the POST response rather than a redirect URL. Consent lists the requested scopes and supports typed approve or deny decisions; regular denials return access_denied to the exact registered callback, while out-of-band denials render a local result. Elk and Phanpy remain independent full Mastodon clients.

Packaging and failures

Install Cargo Leptos 0.3.7 and the wasm-bindgen CLI matching the workspace’s wasm-bindgen dependency (currently 0.2.126). Cargo Leptos downloads the pinned Tailwind CSS 4.3.3 standalone CLI when LEPTOS_TAILWIND_VERSION=v4.3.3 is set. The vendored daisyUI plugins are loaded by that standalone executable, so frontend builds do not require Node.js dependencies. Set that variable alongside LEPTOS_WASM_OPT_VERSION=version_131 when invoking Cargo Leptos; CI, release, and container builds set both automatically. Because Cargo Leptos prefers an existing executable, any wasm-opt already on PATH must also be version 131. cargo leptos build then produces the native binary and target/site. Container and archive releases ship both, and ROOSTY_WEB_ROOT locates the site directory at runtime. The backend serves /pkg from that directory.

The hydrated entry point installs browser-panic-hook before starting Leptos. Browser panics keep their diagnostics in the console while replacing the page body with a small, non-diagnostic recovery view that works without further WebAssembly execution.