The codebase is split into two layers: ddd-core (shared app, build tooling, types, and a working reference conference) and a fork (the actual conference — content, theme, year configs, wrangler bindings). Each layer lives in its own git repo. Forks embed core as a git subtree under core/.
This repo (dddperth) is one such fork. Other forks (e.g. a hypothetical dddadelaide) follow the same shape.
ddd-core/ ← shared layer (its own repo)
website/ React Router + Cloudflare Worker app
app/ components, routes, lib, services
workers/ worker entry
themes/ base.theme.ts (token contract), theme-builder
app/theme/ token primitives (colors, durations, shadows, recipes)
migrations/ D1 schema
vite-plugins/ mdx-bundles, etc.
scripts/ prepare-deploy-config, manifest-d1-migrate
panda.config.ts reads theme via @conference/theme
vite.config.ts reads content paths + wrangler config via @conference/build-manifest
tsconfig.json defines @conference/* path aliases
libs/conference-config/ types-only shared package
src/types.ts ConferenceYear, Sponsor, ConferenceVenue, ...
src/manifest.ts ConferenceManifest (runtime) + ConferenceBuildManifest (build)
src/sessionize-schema.ts Zod schemas
conference-stub/ "DevConf Example" — working sample conference
manifest.ts, build-manifest.ts
config/ public, socials, years/{2024,2025,2026}, venues
content/ pages/, blog/
public/ static assets (placeholder sponsor logos)
theme/ example.theme.ts + example-light.theme.ts + index.ts
wrangler/ local + staging + production with placeholder IDs
<fork>/ ← per-conference repo (e.g. dddperth/, dddadelaide/)
core/ git subtree from ddd-core, never edited directly
conference/ this fork's content + config
manifest.ts runtime manifest: public, socials, brand, conferences
build-manifest.ts build manifest: theme refs, content paths, deployment names
config/ public.ts, socials.ts, years-index.ts, years/, venues/
content/ pages/ (15+ MDX), blog/ (posts + authors.yml)
public/ static assets (sponsor logos, ...) overlaid onto the site root
theme/ <slug>.theme.ts + <slug>-light.theme.ts + index.ts
wrangler/ local.jsonc, staging.jsonc, production.jsonc
package.json proxies pnpm/nx into core/
pnpm-workspace.yaml core/website, core/libs/*, conference
tsconfig.json extends core/tsconfig.base.json, overrides @conference/* paths
.gitattributes core/conference-stub/** merge=ours
conference-stub/ lives in ddd-core and serves two jobs:
- Makes core runnable on its own. A contributor cloning
ddd-corecanpnpm i && pnpm nx dev websiteand get DevConf Example. No fork required. - Seed for new forks.
/new-conferencecopies this folder as the starting point for a new fork's/conference/, then runs a substitution pass for name, domain, slug, etc.
Because it's the seed, any change to the manifest contract, theme shape, or wrangler shape flows through to every new fork automatically — no parallel templates/ folder to keep in sync.
Core reads everything fork-specific through four path aliases defined in website/tsconfig.json:
| Alias | Resolves to | Contents |
|---|---|---|
@conference/manifest |
<conference>/manifest.ts |
Runtime manifest. Importable from anywhere — server, client, components. |
@conference/build-manifest |
<conference>/build-manifest.ts |
Build manifest. Importable from vite.config.ts, panda.config.ts, build scripts. Has Node-only fields. |
@conference/theme |
<conference>/theme/index.ts |
Theme barrel. Imported by panda.config.ts. Separated from build-manifest because Panda CJS-bundles panda.config.ts and that strips import.meta. |
@conference/content/* |
<conference>/content/* |
Used by app/lib/authors.server.ts to import the blog authors YAML via Vite's ?raw loader. |
In ddd-core standalone, those aliases point at ../conference-stub/. In a fork, the fork's tsconfig.json overrides them to point at the fork's own ./conference/. Each repo only knows about one conference — there's no runtime switching.
vite.config.ts and panda.config.ts are loaded by Node directly (before vite-tsconfig-paths is wired up), so they import the build-manifest and theme barrel via relative paths. The /new-conference skill sets these paths when it scaffolds a fork.
The split between runtime and build manifest matters:
ConferenceManifest(runtime) — what the app needs at request time: name, socials, brand info, year configs. No Node imports. Safe in the client bundle.ConferenceBuildManifest(build) — extends runtime withcontent.{pagesDir,blogDir,blogAuthorsFile,publicDir}(computed withpath.resolve(import.meta.dirname, ...);publicDiris optional — theconference-publicvite plugin overlays it onto the site root in dev and copies it intobuild/clienton build),theme.{currentTheme,currentLightTheme}(Panda CSS types), anddeployment.{workerName,d1DatabaseName,webUrl}(per-env names). Only ever imported by build-time code.
Mixing the two ships path.resolve calls into the browser bundle, which fails at runtime under workerd. The type system enforces the split.
Three jsonc files live in conference/wrangler/. All paths inside are relative to the wrangler file's location (e.g. main: "../../core/website/workers/app.ts" from a fork; "../../website/workers/app.ts" in ddd-core standalone since there's no core/ prefix). The Nx deploy / dev targets pass -c .../wrangler/<env>.jsonc to wrangler.
D1 database names are duplicated between the wrangler files and build-manifest.ts deployment.d1DatabaseName. The duplication is intentional — wrangler needs the names statically in JSON, and the Nx D1 migration scripts read them from the manifest. The new-conference skill keeps both in sync; if you edit by hand, change both.
| Question | Where it goes |
|---|---|
| New React component used by all conferences | core/website/app/components/ |
| New route used by all conferences | core/website/app/routes/ |
| New conference year's data | conference/config/years/YYYY.ts |
| Brand colour change | conference/theme/<slug>.theme.ts (and -light) |
| Content of an existing page (about, faq, etc.) | conference/content/pages/*.mdx |
| New page added for one conference only | conference/content/pages/<slug>.mdx (auto-discovered by mdx-bundles) |
| Hero blurb on the homepage | conference/content/pages/_home-hero.mdx, referenced by manifest.homepage.heroBlurbSlug |
| Country / land acknowledgement | conference/content/pages/_acknowledgement.mdx, referenced by manifest.homepage.acknowledgementSlug |
Mobile app download page (/app) |
manifest.mobileApp (iOS + Android URLs). Omit to 404 the route |
| New manifest field | core/libs/conference-config/src/manifest.ts (interface) + every fork's conference/manifest.ts (value) |
| D1 migration | core/website/migrations/ (schema is core; data is per-fork at runtime) |
| Sessionize/Tito secret | env var (set per environment via wrangler secret put) |
The runtime manifest has two opt-in slots for fork-owned MDX rendered by core components:
homepage.heroBlurbSlug— MDX shown in the home-page hero. Without this slug, the hero renders a one-line fallback frommanifest.public.description.homepage.acknowledgementSlug— MDX rendered as the footer Country acknowledgement. Without this slug, the section doesn't render — appropriate for forks in regions without Country acknowledgement conventions.
Slugs starting with _ are excluded from the sitemap and the catchall route (they're fragments embedded in other pages, not navigable pages).
Mobile-app advertising follows a different pattern — manifest.mobileApp either exists (route renders) or doesn't (route + JSON endpoint both 404). No fallback rendering, because pointing visitors at a non-existent app is worse than no link at all.
When a new per-fork extension point is needed, the same pattern applies: add an optional manifest field in core/libs/conference-config/src/manifest.ts, have the core component check + branch on it, ship an .mdx in conference/content/pages/ if the fork wants the content. Avoid baking conference-specific strings into components.
core/website/scripts/prepare-deploy-config.mjs— runs beforewrangler deployto merge env-specific vars fromconference/wrangler/<env>.jsoncinto the built worker config.core/website/scripts/manifest-d1-migrate.mjs— reads D1 database name for the chosen env fromconference/manifest.tsand shells out towrangler d1 migrations apply. Letspnpm nx d1-migrate-{local,staging,production} websitework without hard-coding conference names in core.
Two skills automate the lifecycle:
/new-conference(in.claude/skills/new-conference/) — scaffolds a fork repo withcore/as a git subtree. Copiesconference-stub/as the seed for the fork's/conference/, then runs a substitution pass. No separate templates folder./pull-upstream(in.claude/skills/pull-upstream/) — pulls latest ddd-core into a fork viagit subtree pulland verifies the build.
See each skill's SKILL.md for the full workflow.