Skip to main content

Documentation

  1. The package index
  2. django-brickwork
  3. Adopting brickwork into an existing app (the strangle pattern)

Adopting brickwork into an existing app (the strangle pattern)

INTEGRATION.md covers a greenfield install. This guide covers the harder, more common case: strangling an existing UI kit (a hand-built shell plus a c-/x- component library) onto brickwork cluster by cluster, keeping the old kit alive until the last screen moves. It is a checklist distilled from a real console cutover, not a theory; the structure below is reusable for the next brownfield adopter.

Principle: strangle, do not big-bang

brickwork is stable (3.x) but still releases frequently, and a whole-app commit carries real churn risk: a fast-moving minor could touch a contract you depend on across every screen at once. Migrate incrementally so that at any moment most of the app still runs on the proven old kit and only the migrated cluster carries brickwork risk. Keep rollback trivial at every step.

The sequence

1. Pilot one screen cluster first

Pick one coherent cluster (a settings area, one resource's list+detail+form) and move only that. The pilot's job is to prove the four load-bearing seams end to end on real screens:

  • the shell (does your layout survive inside brickwork/shell/app.html?),
  • the nav (does your route tree map onto NavItem, including any parameterised / multi-view sections?),
  • the forms (does your validation flow map onto the 422 loop, or the no-JS full-page redisplay floor?),
  • the JS-bearing body (does your chart / editor mount cleanly inside {% block content %} with your own asset pipeline coexisting?).

Wire each per INTEGRATION.md. If all four hold on the pilot cluster, the rest is repetition; if one fights you, you have found the real adoption cost before committing the whole app.

2. Migrate cluster by cluster, each its own PR

One cluster per PR keeps every step reviewable and independently revertable. Do not let a half-migrated cluster straddle a release. The old kit and the brickwork shell coexist for the duration: two base templates live side by side, screens extend whichever kit they currently belong to, and you move screens across one cluster at a time. This is expected and fine; plan for months of coexistence, not a weekend.

3. Draw the boundary explicitly

Decide, up front and in writing, what migrates and what does not:

  • Migrates: all reusable interface design: chrome, forms, list and detail screens, dashboards, public-site patterns, documentation and editorial layouts, transactional journeys, and the frames and interaction conventions around data visualisation.
  • Stays consumer-owned: product-specific semantics, business rules, permissions, integrations, content operations and any specialist renderer a project selects. A charting engine may stay app-owned, but it belongs inside Brickwork's layout, token, state, legend, tooltip and filter contract. A separately branded or hosted public site is migrated deliberately rather than accidentally during a console cutover. Note (ADR-055, from v1.2.0): brickwork now also ships an opt-in brickwork.marketing sub-app (add "brickwork.marketing" to INSTALLED_APPS) with a marketing shell and nine marketing components (hero, feature grid, pricing tier, pricing table, CTA, testimonial, logo cloud, stat band, FAQ) on the same --bw-* token / brand / accessibility contract. Worked landing/pricing/about pages built on that shell and those components ship as copy-paste examples (brickwork.examples), not importable templates (ADR-056, from v2.0.0): a whole page is the most project-specific thing a consumer owns, so it is copied in and edited, never extended. If a consumer wants its marketing pages on brickwork too (the "one package covers all templating" case), that is now a supported opt-in, migrated on its own schedule, not forced into the console cutover. What brickwork still does not ship is a general-purpose public-website theme engine (no page builder, no tenant-arbitrary content or CSS/JS); the marketing kit is a fixed, curated set of components and shells, not a CMS. The marketing kit is the first slice of a wider templates-catalogue trajectory (layout variants, then themed starter kits, each demand-gated); see oss/docs/plans/brickwork-templates-catalogue-direction.md in the umbrella.

Writing the boundary down stops scope creep mid-migration, where "while we are in here" quietly pulls non-goals into the cutover.

If your editorial content comes from a block-based CMS rather than a single rich-text field, wiring it onto .bw-prose has one extra step: see INTEGRATION.md section 11 before you migrate the first content-heavy cluster, not after a page ships with flat paragraphs.

4. Gate the final "delete the old kit" step on a stability signal

The last step, removing the old kit entirely, is the one with no rollback. Gate it on a stability signal rather than doing it the moment the last screen moves: a brickwork release you have pinned and validated across the migrated app. Pin to a major you trust with an open range, >=3.2,<4, so patch and minor releases land automatically while a major bump (which may carry a clean break, per brickwork's own no-deprecation-window policy on majors) requires a deliberate version bump on your side. Until then, keeping the old kit's code in the tree (even with no screens using it) costs nothing and preserves the escape hatch.

Three wrinkles a real brownfield cutover hits (brickwork#49, brickwork#75)

Multi-host projects (the shell branches per host)

A project served on several hosts (django-hosts: a merchant host, an agency host, an account host, each branching the shell) must decide per host whether that host's screens are in the migration. The clean pattern: each host resolves its own base template, and you migrate host by host as well as cluster by cluster. A host still on the old shell and a host on the brickwork shell coexist with no shared state beyond your own context; the per-request brand / theme resolver (BRICKWORK_THEME_RESOLVER, see INTEGRATION.md section 3) is where a multi-host app branches brand per host. Do not try to run one shared base template across hosts mid-migration; let each host cross the line on its own schedule.

Legacy-shell / brickwork-shell asset coexistence

While both shells are live, both asset pipelines are live. The classic pairing is django-vite hashed bundles (the legacy kit) alongside brickwork's plain {% static %} artefacts. These coexist cleanly: brickwork never enters your bundler, and your bundler never needs to touch brickwork's static. Points to hold:

  • Load brickwork's stylesheet through the shell (it does this for you) and your brand override through head_extra; load your Vite bundles the way you already do. Two independent <link> / <script> sets on the page is correct, not a smell.
  • Do not route brickwork's brickwork.css through Vite to "unify" the pipeline; that couples you to brickwork's internal asset layout, which is not a contract.
  • If you run Alpine yourself (host-owned Alpine.start()), brickwork registers its interaction components against your instance; keep a single Alpine start, not one per kit.

A second component framework in the content block (brickwork#75)

Some adopters do not strangle a hand-built kit; their in-app component layer is itself a framework (django-components is the named case: @register(...) components consumed via {% component "name" %}). The contract, stated explicitly so the migration boundary can be drawn with confidence:

A second component framework rendering inside {% block content %} (and inside form bodies and card bodies) is supported and safe, the same way app-owned charts are. brickwork's components are plain template tags and {% include %} partials; they claim no ownership of what renders inside the body slots, and nothing in the shell inspects or interferes with markup another framework produces there. This is a legitimate END STATE, not just a migration phase: brickwork owns the shell, nav, forms scaffolding and generic components; your component framework owns the domain components, mounted in brickwork slots, indefinitely.

The boundary rule that follows: Brickwork owns reusable interface patterns; your framework owns product-specific semantics and logic. A calendar grid that builds availability matrices in get_context_data, a price display doing currency formatting, or a rating widget can remain on a consumer framework until a generic pattern is proven. Do not force an abstraction early; mount it inside Brickwork's layout and interaction contract.

Do:

  • Mount framework components inside {% block content %}, card bodies, and form bodies exactly as you would app-owned markup.
  • Keep ONE Alpine: the host owns the single Alpine.start(); brickwork registers against it, and a component framework's JS must not boot a second Alpine (or a second htmx) instance.
  • Emit the framework's dependency tags (django-components' {% component_css_dependencies %} / {% component_js_dependencies %}, which inject <link>/<script> from a component registry) through the shell's blocks: stylesheets in head_extra after {{ block.super }}, so brickwork's stylesheet and your brand override keep their documented cascade order (a component stylesheet that overrides --bw-* tokens then wins only where it should); scripts in body_js. Two independent dependency systems on one page is correct, not a smell (the same ruling as the asset-pipeline wrinkle above).
  • If you run a nonce-based CSP, confirm the framework's injected tags carry your nonce; brickwork's shell blocks are ordinary template blocks, so whatever nonce discipline your templates use passes through unchanged.

Do not:

  • Do not rewrite domain components as brickwork tags (the boundary rule above).
  • Do not route brickwork's static through the framework's dependency system (the mirror of the "do not route brickwork.css through Vite" rule).
  • Do not mount a framework component that renders its own <html> document or page shell inside a brickwork slot; body slots take fragments.
  • Do not reach from a framework component into .bw-* class internals; style your components on your own classes plus the --bw-* tokens.

The brickwork suite keeps this promise executable: the consumer smoke harness (tests/consumer/) renders a simulated second-framework component, with its own injected stylesheet and script dependencies, inside the shell's content block, without depending on django-components itself.

The htmx floor, if you are brownfield on htmx 1.9 (brickwork#48)

brickwork's interaction contracts are built and CI-gated on htmx >= 2.0 only (BR-BW-HTMX-010); htmx 1.9 is out of contract. If your app is on htmx 1.9, treat the htmx 1 -> 2 upgrade as a prerequisite workstream that lands before the brickwork cutover, not something to reconcile screen by screen mid-migration. htmx 2 changed default response handling in ways the 422 swap loop relies on; running the cutover on 1.9 means wiring htmx:beforeSwap by hand on a path brickwork does not test. Sequence the htmx upgrade first, then strangle onto brickwork on a supported floor.

What a worked instance looks like

The console cutover this guide is distilled from ran as an ADR plus a staged plan: one ADR recording the decision and boundary, then a plan enumerating the clusters in migration order with the "delete the old kit" step gated last. That shape (decision + boundary in an ADR, ordered clusters in a plan) is worth copying: it makes the migration auditable and the boundary durable against scope creep.

Documentation