logo

Frontend

Web app, shared UI, i18n, prerendering, SEO, and Chrome extension

OPCStack has two frontend entrypoints: the SvelteKit web app and the WXT Chrome extension. They share UI, config, i18n, and API client code through src/frontend/lib/.

The rule is simple: shared frontend code belongs in lib/; entrypoint-specific code stays in web/ or extension/.

Multi-entrypoint Architecture

src/frontend/
  lib/
    app-ui/
    config/
    i18n/
    styles/
    ui/
  web/
    routes/
    hooks.server.ts
    app.html
    static/
  extension/
    entrypoints/
    public/
    wxt.config.ts

src/frontend/lib/ must not depend on SvelteKit route files, server-only APIs, or WXT-only APIs. The web app and extension can import from lib/, but lib/ should not import back from either entrypoint.

The important aliases:

Alias Points to Used for
$frontend src/frontend/lib UI, config, i18n, styles
$apiContract src/api-contract Typed API client and shared request/response types
$backend src/backend Server-only imports

Directory Layout

src/frontend/lib/ui/ contains reusable UI primitives. These are shadcn-svelte style primitives such as Button, Card, Dialog, Alert, Empty, inputs, tables, dropdowns, and form controls.

src/frontend/lib/app-ui/ contains composed business UI. Current groups:

Directory Purpose
auth/ Login, register, OTP, reset password, Turnstile, legal copy
shell/ App header, user menu, theme switcher, locale switcher
landing/ Landing page visual blocks

src/frontend/web/routes/ contains SvelteKit routes. Most business pages should live under [locale=locale]/ so the URL carries the locale.

src/frontend/extension/entrypoints/ contains WXT entrypoints:

Entrypoint Purpose
background.ts Background worker and message listener
page.content.ts Content script matched by extension host permissions
popup/ Extension popup UI
options/ Extension options UI

Adding a Page

Add localized business pages under src/frontend/web/routes/[locale=locale]/.

Example:

src/frontend/web/routes/[locale=locale]/settings/+page.svelte

Use typed props for data:

<script lang="ts">
	let {
		data
	}: {
		data: {
			locale: string
			siteName: string
			canonicalUrl: string
		}
	} = $props()
</script>

Use <svelte:head> on business pages:

<svelte:head>
	<title>{title} - {data.siteName}</title>
	<meta name="description" content={description} />
	<link rel="canonical" href={data.canonicalUrl} />
</svelte:head>

Reuse existing primitives and app UI:

<script lang="ts">
	import AppHeader from '$frontend/app-ui/shell/AppHeader.svelte'
	import { Button } from '$frontend/ui/button'
</script>

Do not rebuild primitives that already exist. Use the components from $frontend/ui/* and $frontend/app-ui/*.

UI Primitives

Use $frontend/ui/* for low-level UI:

  • button
  • card
  • dialog
  • alert
  • empty
  • field
  • input
  • table
  • dropdown-menu

If a list can be empty, use the Empty family from $frontend/ui/empty. Do not render plain ad hoc empty text.

Use icons from @lucide/svelte. Do not add another icon library.

App UI Components

Auth UI already exists in src/frontend/lib/app-ui/auth/:

Component Purpose
LoginCard Email/password login and social login buttons
RegisterCard Email signup and optional OTP trigger
OtpCard Email OTP input and resend cooldown
ForgotPasswordCard Request password reset
ResetPasswordCard Reset password with OTP
Turnstile Cloudflare Turnstile widget wrapper
LegalDisclosure Terms, privacy, and optional refund links

Shell UI lives in src/frontend/lib/app-ui/shell/:

Component Purpose
AppHeader Top navigation
UserMenu Current user avatar, settings, sign out
ThemeSwitcher Theme mode control
LocaleSwitcher Locale switching

Business pages should compose these components instead of copying their internals.

Typed API Client

The frontend imports client from $apiContract/client. Do not instantiate createAuthClient directly in pages.

Web app default:

import { client } from '$apiContract/client'

await client.auth.signIn.email({ email, password })
await client.auth.signOut()

const session = client.auth.useSession()

The default browser client uses cookies and bookmark cookies:

export const client = createClient({
	baseUrl: typeof window === 'undefined' ? '' : window.location.origin,
	auth: { type: 'cookie' },
	bookmarks: { type: 'cookie' }
})

Extension code creates a token-based client:

const client = createClient({
	baseUrl: clientConfig.apiBaseUrl,
	auth: { type: 'token', storage: tokenStorage },
	bookmarks: { type: 'storage', storage: createMemoryBookmarkStorage() }
})

Business APIs are available under client.api.*, for example:

await client.api.getCreditSummary()
await client.api.listNotifications({ page: 1, page_size: 20 })

The client also carries D1 bookmark headers between requests, so pages should not manage x-d1-meta-bookmark or x-d1-tenant-bookmark manually.

i18n

OPCStack uses svelte-i18n.

Messages live in:

src/frontend/lib/i18n/messages/en.json
src/frontend/lib/i18n/messages/zh.json

The locale is carried by the route segment:

/:locale/...

The [locale=locale] route parameter uses src/frontend/web/params/locale.ts to accept only supported locales. Current supported locales are en and zh.

Use $_('key') in Svelte components:

<script lang="ts">
	import { _ } from '$frontend/i18n'
</script>

<h1>{$_('settings.title')}</h1>

src/frontend/web/hooks.server.ts redirects non-localized pages to the best locale based on Accept-Language, unless the path is an API, asset, legal page, robots, sitemap, favicon, manifest, or internal Cloudflare path.

Styling and Design System

Global styles live in src/frontend/lib/styles/app.css. The root layout imports it once:

import '$frontend/styles/app.css'

The active design system comes from public config:

DESIGN_SYSTEM=apple-saas

Supported values in CSS:

apple-saas
brutalism

The selected design is written to <html data-design="..."> by the SvelteKit hook and the client layout. demo-design/[theme] can override it by URL.

Use semantic Tailwind tokens instead of raw colors in page code:

<div class="border border-border bg-background text-foreground">
	<p class="text-muted-foreground">...</p>
</div>

Concrete values for colors, radius, typography, shadows, and design-specific overrides belong in app.css, not scattered across pages.

Public Config

Frontend config is generated into:

src/frontend/lib/config/client.generated.ts

Import it through:

import { clientConfig } from '$frontend/config/client'

scripts/prepare-public.mjs generates the file from .env.dev, .env.prod, .env, and process env. It also prepares web logo and extension icons.

Common fields:

Field Used for
appName Titles, extension manifest, app UI
appVersion Extension manifest
apiBaseUrl Extension API client
webBaseUrl Canonical URLs and extension navigation
supportEmail Legal and support pages
designSystem CSS design variant
docsEnabled Docs route enablement
emailSignupEnabled Auth UI
googleAuthEnabled Auth UI
githubAuthEnabled Auth UI
linuxdoAuthEnabled Auth UI
turnstileEnabled Auth UI
paymentEnabled Legal refund link and payment UI
extension.hostPermissions WXT manifest and content script matches

Frontend code must not read secret env files.

Prerender

Use export const prerender = true only when the page has:

  • no per-request server data
  • no auth
  • no dynamic DB query
  • identical content for all visitors

Current prerendered routes:

Route Reason
robots.txt Static text generated from public config
sitemap.xml Static route and docs index
/[locale]/docs/* Markdown docs from public-docs/
/[locale]/demo-design/[theme] Static design preview entries

Dynamic params need entries(). Docs generate all { locale, slug } pairs from the docs manifest. Design demo generates all { locale, theme } pairs from supported locales and theme names.

Do not prerender pages that depend on Better Auth session, user-specific API calls, or per-request database reads.

SEO

src/frontend/web/routes/+layout.server.ts provides shared SEO data:

  • siteName
  • supportEmail
  • siteUrl
  • logoUrl
  • canonicalUrl
  • alternateUrls
  • xDefaultUrl
  • websiteJsonLd

Business pages should set:

<svelte:head>
	<title>{title} - {data.siteName}</title>
	<meta name="description" content={description} />
	<link rel="canonical" href={data.canonicalUrl} />
</svelte:head>

Docs pages also set alternate locale links and Open Graph/Twitter metadata.

Canonical URLs are built from clientConfig.webBaseUrl, which is generated from APP_DOMAIN. Do not hardcode OPCStack website URLs into business pages.

Public Docs

Product docs rendered by the web app live in:

public-docs/en/
public-docs/zh/

Docs route:

/:locale/docs/[...slug]

Docs are enabled by:

DOCS_ENABLED=true

Markdown files use frontmatter:

---
title: Authentication
description: Email and Google sign in configuration
group: Guides
order: 1
---

Docs images should live under src/frontend/web/static/images/ and be referenced as /images/....

template-docs/ is different. It is for template maintainers and agents, not rendered by the app.

Chrome Extension

The extension uses WXT with Manifest V3:

pnpm dev:extension
pnpm build:extension

Both commands run prepare:public:* first, so client.generated.ts, icons, and public config are available.

src/frontend/extension/wxt.config.ts builds the manifest from clientConfig:

Manifest field Source
name clientConfig.appName
version clientConfig.appVersion
description ${clientConfig.appName} browser extension
host_permissions clientConfig.extension.hostPermissions
icons Generated icons in src/frontend/extension/public/icons/

Extension entrypoints:

File Purpose
background.ts Handles runtime messages
page.content.ts Sends a content ping from matched pages
popup/Popup.svelte Token-based client example
options/Options.svelte Basic options page

The extension imports $frontend from src/frontend/lib/. Keep extension-only APIs such as wxt/browser inside src/frontend/extension/.

Common Mistakes

Importing web routes from shared lib. src/frontend/lib/ must not import from src/frontend/web/ or src/frontend/extension/. Shared code should be reusable by both.

Creating a raw auth client in pages. Use $apiContract/client. It already handles auth mode, token sync, and D1 bookmarks.

Forgetting localized routes. Business pages should usually be under [locale=locale]/, not at the root. Root-level pages are for legal/static exceptions.

Prerendering authenticated pages. Anything depending on session or user-specific data should not set prerender = true.

Hardcoding public config. Use clientConfig. If a new public value is needed, add it to public env and prepare-public.mjs; do not read secret env files in frontend code.

Duplicating UI primitives. If Button, Dialog, Alert, Empty, Field, Input, or table primitives already exist, use them.