---
title: Migrate from django-components
url: https://citry.dev/guides/migrate-from-django-components/
description: "Move a django-components project to Citry with a complete, agent-friendly checklist of template, Python, asset, extension, and testing changes."
---
# Migrate from django-components

If you want to move an existing django-components project to Citry, start with
one leaf component, keep the existing page working, and use the checklist below
to find every django-components pattern that needs attention.

This guide compares Citry's current documented behavior with the
django-components source at commit
[`5d4d4f5`](https://github.com/django-components/django-components/commit/5d4d4f5d13dd06c80ba389f30fc63fdbb71cda75){: target="_blank" rel="noopener"}
from June 20, 2026. If your project uses another django-components version,
check its release notes for additional differences. Select the documentation
version that matches the Citry version installed in your project.

## Choose how to migrate

For a gradual migration inside Django, use
[`citry-django`](https://github.com/joeyjurjens/citry-django){: target="_blank" rel="noopener"}
to place Citry components in Django templates and use Django template tags in
Citry components. It is a community integration, so follow its README for the
versions and setup it supports.

For a direct port, render Citry from its
[Django integration](/web-frameworks/#django) and replace a complete component
subtree at a time. The two integrations solve different problems: Citry's
integration mounts rendering, assets, and event routes; `citry-django` lets the
two template languages coexist while you migrate.

Before changing components:

1. Create a branch and run the existing Python, browser, and snapshot tests.
2. Inventory component directories, settings, custom template tags,
   extensions, JavaScript hooks, caches, and `Component.View` subclasses.
3. Install Citry, create one `Citry` instance, connect it to Django, and
   register or discover one leaf component.
4. Port that component and its tests. Verify it in the browser before moving
   to the next connected group.
5. Remove django-components only after searches and tests show that nothing
   still depends on it.

Citry intentionally does not include django-components compatibility aliases
or silent shims. A leftover pattern should be migrated explicitly.

## How to read the checklist

Every known user-visible divergence is included below. Use the stable
`DJC-###` identifier when tracking work or asking for help.

- **🔴 Breaks:** the project will fail until the pattern is changed.
- **🟡 Check behavior:** output or runtime behavior may differ.
- **🟢 Update tests:** browser behavior is equivalent, but exact output or an
  assertion may need updating.

Search signatures are written in the django-components column. Not every row
applies to every project, so record non-matches as not applicable instead of
making speculative changes.

## HTML attributes

Rewrite attribute merging first because these differences are easy to find and can silently change output.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-001">DJC-001</span> | Merging HTML attributes | The `{% html_attrs %}` tag (positional args, `attrs:` / `defaults:` aggregate keys, spread) | Element-level attributes: `c-bind="mapping"` to spread, plus `c-class` and `c-style` | Rewrite `{% html_attrs attrs defaults class=... %}` as `&lt;div c-bind="defaults" c-bind="attrs" c-class="..."&gt;`. Attributes apply left to right and the later one wins, so put the fallback mapping first and the caller's mapping after it. `class` and `style` merge instead of overwriting. A leftover `attrs:foo=` is not rejected: it arrives as an input literally named `attrs:foo`, so search for `:` in attribute names. | 🔴 |
| <span id="djc-002">DJC-002</span> | Repeated non-`class`/`style` attribute keys | The same key supplied twice is space-joined (`foo="bar baz"`) | Last-one-wins (`foo="baz"`) | If you relied on a repeated plain attribute concatenating, combine the value yourself. `class` and `style` still merge. | 🟡 |

## Rendering, context, and inheritance

Make component dependencies explicit and remove assumptions inherited from Django template context.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-007">DJC-007</span> | Component template must be well-formed | A component's `template` is arbitrary text passed to the Django template engine; unclosed tags are tolerated | An unclosed or mismatched tag is an error: loading the component fails with `SyntaxError: Unclosed tag &lt;thead&gt;` | Close every tag in a component `template`. A partial that was a bare `&lt;thead&gt;` fragment has to become a complete unit, for example by including its `&lt;table&gt;` wrapper and passing the rows in as a slot. | 🟡 |
| <span id="djc-008">DJC-008</span> | Ambient template context | A component can read variables that are simply in the surrounding `Context`, and exposes `self.outer_context` | A component receives only its explicit props (kwargs) and slots; there is no ambient context and no `outer_context` | Pass every value a component needs as an explicit prop. For caller state that must reach deep descendants, use `provide` / `inject`. | 🔴 |
| <span id="djc-009">DJC-009</span> | `context_behavior` setting and `only` | `context_behavior` chooses `django` (child sees outer context) vs `isolated`, and `only` forces isolation per call | citry is always isolated, as if `only` were always on | Remove `context_behavior` from settings and drop `only`; behavior already matches djc's `isolated`. A project that ran in `django` mode must also rewrite fills that read the child's variables (loop items and friends): pass them explicitly as scoped slot data (`c-name=` on the slot site, `data=` on the fill). | 🟡 |
| <span id="djc-010">DJC-010</span> | Request, context processors, CSRF | `self.request`, context-processor variables, and `csrf_token` are injected into the template context | citry injects no ambient request-derived variables | Read the request in your view and pass what each component needs (CSRF token, current user, locale) as ordinary props. There is no per-request ambient context, so a value many components need is best provided once near the top of the page with `&lt;c-provide&gt;` and read with `inject()`. Follow [CSRF protection](/security/#protect-event-posts-from-csrf) for Django and Citry Events. | 🔴 |
| <span id="djc-011">DJC-011</span> | Slot-filled introspection | `{% if component_vars.is_filled.title %}` branches on whether a slot was filled | The `component_vars.is_filled` magic variable is gone | In `template_data` compute `{'has_title': slots.get('title') is not None}`, then branch with `&lt;c-if&gt;`. | 🟡 |
| <span id="djc-013">DJC-013</span> | Observing which components rendered | The Django `template_rendered` signal and `assertTemplateUsed` report what rendered | citry has no template signal | Replace signal receivers / `assertTemplateUsed` checks with a test extension that records `on_component_rendered`. | 🟡 |
| <span id="djc-014">DJC-014</span> | Django template inheritance | Component templates use `{% extends %}` / `{% block %}`, and `{% include %}` pulls in partials | citry has no template inheritance or `{% include %}` | Restructure an `{% extends %}` template into a base component composed via slots; replace `{% include 'p.html' %}` with a `&lt;c-p /&gt;` component. | 🔴 |

## Template syntax and expressions

Citry templates use HTML-shaped component tags and Python-shaped expressions rather than Django template tags and filters.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-015">DJC-015</span> | Component invocation syntax | A pluggable `TagFormatter` / `ShorthandComponentFormatter` customizes the `{% component %}` tag form | citry's syntax is the fixed `&lt;c-*&gt;` form; there is no formatter to configure | Remove any `tag_formatter` setting and custom formatter subclasses; write components as `&lt;c-name /&gt;`. | 🔴 |
| <span id="djc-016">DJC-016</span> | django-template-partials integration | Rendering `template.html#partial_name` where the partial contains components | No direct equivalent | Compose the partial as a citry component and render it directly. | 🔴 |
| <span id="djc-017">DJC-017</span> | Unterminated expression/comment delimiters | An opened `{{` or `{#` with no closing delimiter falls back to visible text | Citry raises `SyntaxError` when the component is loaded, before anything renders | Close the expression/comment delimiter; do not rely on malformed template syntax rendering literally. | 🟡 |
| <span id="djc-018">DJC-018</span> | Django template filters | Tag values use `value\|filter:arg`, filter registries, chaining, and filter-specific whitespace/arity rules | Citry has no template filters; `\|` inside an expression is Python bitwise-or | Rewrite filters as Python expressions, for example `value.upper()`, `'yes' if value else 'no'`, or an explicitly supplied helper callable. | 🔴 |
| <span id="djc-019">DJC-019</span> | Translation shorthand in component inputs | `_('text')` is a special translation value inside arguments, filter arguments, lists, and dicts | Citry has no translation token/backend; the default sandbox also rejects the variable name `_` | Translate in `template_data`, or expose a non-underscore callable such as `translate` through template globals and use `c-label="translate('Hello')"`. | 🔴 |
| <span id="djc-020">DJC-020</span> | Positional component inputs and list spreads | Tags accept positional values and `...list`, with Python-like positional/keyword ordering rules | Component invocations are kwargs-only; `c-bind` spreads mappings, not positional lists | Give every input a name. Replace a positional list spread with a mapping and `c-bind`, or model the list as one named prop. | 🔴 |
| <span id="djc-021">DJC-021</span> | Parser-registered tag flags | A `TagSpec` can declare flags that affect parsing but are omitted from the component's args/kwargs | There are no parser flags. A bare attribute is a normal input with the value `True` | Convert each custom flag into an explicit boolean prop and handle it in the component. | 🔴 |
| <span id="djc-022">DJC-022</span> | `$component` callback payload | The first callback argument is the component's JS-data object, with a separate context argument | One object is passed: `{id, els, data}`. Extensions may add more members to it | Rewrite the django-components callback as `$component(({data, els, id}) =&gt; { const {message} = data; ... })`. Handle `data === null` when `js_data()` returns no values. | 🔴 |
| <span id="djc-023">DJC-023</span> | Dependency placement tags | `{% component_css_dependencies %}` emits CSS only; `{% component_js_dependencies %}` emits JS only | `&lt;c-css /&gt;` and `&lt;c-js /&gt;` only choose *where* the styles or scripts go. Leave one out and those assets still land on the page, in their usual place | Use the tags for placement only. If you left `{% component_css_dependencies %}` out of a page to keep its CSS off, that no longer works. To keep an asset off the page, remove it from the component or filter it in `on_dependencies`. | 🟡 |
| <span id="djc-024">DJC-024</span> | Component names containing `/` | The string-form component tag can address a registry name such as `te-s/t` | Component names are HTML-tag-compatible: they start with a letter and contain only letters, digits, hyphens, underscores, or dots | Rename a slash-delimited registry key, for example `te-s/t` to `te-s-t` or `te.s.t`, and update the `&lt;c-*&gt;` invocation. | 🔴 |
| <span id="djc-025">DJC-025</span> | Alpine ownership and load order | Alpine is an external dependency; placing it before component JS can make an `alpine:init` listener miss the event | Citry Events loads and starts its own copy of Alpine. If the page has already loaded Alpine, citry leaves yours running and logs a warning | On pages that use Citry Events, delete your own Alpine `&lt;script&gt;` and stop ordering it against component JS; an `alpine:init` listener will no longer miss the event. Your existing `x-` attributes keep working untouched. | 🔴 |
| <span id="djc-033">DJC-033</span> | Attribute evaluation (the biggest trap) | An attribute value is evaluated by the template engine, and `{{ }}` interpolates inside a quoted value | A plain attribute value is taken literally: only a `c-`-prefixed attribute is evaluated, and `{{ }}` written into a static value renders **literally** (`class="{{ kind }}"` outputs `class="{{ kind }}"`, with no error) | Add a `c-` prefix to any attribute whose value must be evaluated. `key="hi"` passes the string `"hi"`; `c-key="hi"` evaluates `hi`. Rewrite `class="{{ x }}"` as `c-class="x"`. This one fails silently, so grep your templates for `{{` inside an attribute. | 🔴 |
| <span id="djc-037">DJC-037</span> | The `{% %}` tag language | Values and bodies may contain any registered block tag, for example `{% lorem n w %}` or a custom tag, including inside a component argument | There is no `{% %}` tag language. Text written that way is not executed; it renders to the page exactly as typed | Compute the value in Python and pass it as an expression attribute (`c-flag="is_active"`), or move the logic into `template_data`. Control flow is `&lt;c-if&gt;` / `&lt;c-for&gt;`. | 🔴 |
| <span id="djc-039">DJC-039</span> | Mixed literal text plus expression in one value | `bool_var=" {% noop is_active %} "` yields the string `" True "`: stray whitespace silently turns a typed value into a string | A `c-` value is one expression, so you get exactly the type the expression returns. (A value holding a whole `&lt;c-*&gt;` tag is a nested component instead, see [DJC-042](#djc-042)) | Build the string yourself where you want one: `c-label="f' {is_active} '"`. The accidental downgrade cannot happen. | 🟡 |
| <span id="djc-040">DJC-040</span> | Template comment placement | `{# #}` works anywhere, including inside a component argument, where it collapses to `""` | A comment can sit between tags or between attributes (`&lt;a {# note #} class="x"&gt;`), but not inside an attribute value: in a plain attribute it renders as visible text, and in a `c-` attribute it is an error | Move every `{# #}` out of attribute values: put it before the attribute, or on its own line above the tag. `title="{# note #}Hi"` would ship the comment to the browser. | 🟡 |
| <span id="djc-042">DJC-042</span> | Passing markup as an input | A whole `{% component 'card' ... / %}` written inside an argument renders to HTML, and that HTML becomes the outer input | A `c-` value that starts with an HTML tag and ends with its closing tag is a **nested template** rather than an expression: real markup, rendered with the same data, so `{{ }}` works inside it. It is the one place a `c-` value is not a Python expression | Write the markup straight into the value: `c-body="&lt;span&gt;Hello {{ name }}&lt;/span&gt;"`. Any HTML works, including several roots (`&lt;em&gt;a&lt;/em&gt;&lt;em&gt;b&lt;/em&gt;`), a self-closing tag (`&lt;br/&gt;`), or a component (`&lt;c-badge c-label='name' /&gt;`). Anything else is still an expression, so plain text needs quotes: `c-body="'hello'"`. Write tags complete: a half-open tag is an error. A nested component renders after the outer one, so its finished HTML is not available inside the outer component's `template_data`. | 🟡 |
| <span id="djc-043">DJC-043</span> | The same input given in both forms | No such concept; there is one argument syntax | Writing `title="x"` and `c-title="y"` on one tag is always a parse-time error because both explicitly provide the same logical input. Plain-element `class`/`c-class` and `style`/`c-style` are the accumulating exceptions. A `c-bind` spread may interlace with one explicit spelling because the key may be absent at render time. Repeating the *same* spelling twice is always an error | Pick one explicit form per input. Preserve intentional class/style accumulation on elements; move conditional overrides into `c-bind`. | 🟡 |
| <span id="djc-054">DJC-054</span> | Authoring custom template tags | Subclass `BaseNode` (tag, end_tag, allowed_flags) or decorate a function with `@template_tag`; inputs follow the render function's Python signature | There is no tag-registration API. The one user-defined tag is a registered component: `&lt;c-my-tag /&gt;` looks the name up in the registry, and an unknown name fails at render naming the tag | Rewrite each custom tag as a component: the render function's body moves into `template_data` or `on_render`, its parameters become `Kwargs` fields, the tag body arrives as the default slot, and flags convert as in [DJC-021](#djc-021). | 🔴 |

## Slots, provide, and inject

Port composition together so slot data and values shared with descendants stay explicit.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-029">DJC-029</span> | Choosing the implicit/default slot | A `default` flag can mark an arbitrary named `{% slot "main" default %}` as the target of implicit component-body content | Implicit body content always fills the literal slot name `default`, rendered by a bare `&lt;c-slot /&gt;` (or `&lt;c-slot name="default" /&gt;`) | Rename the receiving slot to `default`, or keep its name and wrap caller content in an explicit `&lt;c-fill name="main"&gt;`. | 🔴 |
| <span id="djc-030">DJC-030</span> | Missing template variables | An absent Django template variable renders as an empty string | A name that is not defined raises `KeyError`, pointing at the line and column where it is used, so a typo fails loudly instead of rendering an empty string | Supply every referenced name, guard the expression/branch, or compute an explicit default in `template_data`. | 🟡 |
| <span id="djc-031">DJC-031</span> | Slot callbacks and forwarding existing Slots | `SlotContext` exposes a Django `Context`, fallback uses `SlotFallback`, and `{% fill body=my_slot %}` forwards a Slot | `SlotContext` exposes `data`, `fallback: Slot \| None`, and `provides`; there is no Django Context or `body=` shortcut | Remove callback reads from `ctx.context`, treat `ctx.fallback` as an ordinary optional Slot, and forward with `&lt;c-fill name="x"&gt;{{ my_slot }}&lt;/c-fill&gt;`. | 🔴 |
| <span id="djc-032">DJC-032</span> | Legacy fill fallback alias | `{% fill "x" default="fallback_var" %}` remains as a deprecated alias | Only the explicit `fallback="fallback_var"` attribute is accepted | Rename `default=` to `fallback=` on every fill that binds the receiving slot's fallback. | 🔴 |
| <span id="djc-034">DJC-034</span> | The `{% provide %}` tag | `{% provide name key=val var:field=... %}...{% endprovide %}`: a positional `name`, and `var:field=` colon-prefix aggregate kwargs | `&lt;c-provide key="name" ...&gt;...&lt;/c-provide&gt;`: the name is the `key` attribute (`c-key` for a computed one). Each `var:field=` group becomes one attribute holding a dict | Rewrite the block as `&lt;c-provide&gt;` and move the positional name to `key=`. Turn each group into one dict attribute: `{% provide "x" var1:key="hi" %}` becomes `&lt;c-provide key="x" c-var1="{'key': 'hi'}"&gt;`. | 🟡 |
| <span id="djc-035">DJC-035</span> | Injected payload type | `inject(...)` returns a `DepInject` NamedTuple | `inject(...)` returns a `Provided` NamedTuple | Field access (`payload.field`) and tuple behaviour are unchanged; the only observable difference is the type name in the `repr`. Update any assertion or logging that matches the payload's type name or repr. | 🟡 |
| <span id="djc-036">DJC-036</span> | provide / inject key errors | A missing/empty/invalid provide name raises `TypeError` / `TemplateSyntaxError`; a missing inject key raises `KeyError` | An invalid provide key raises `ValueError`. A missing inject key still raises `KeyError`, now with a suggestion of the closest key that was provided | Update `except` clauses and assertions that match the old exception types or message text. | 🟡 |

## Assets and browser startup

Replace Django static-file assumptions and verify how each migrated page starts its browser behavior.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-004">DJC-004</span> | Dependency rendering strategy | `render_dependencies(html, strategy=...)` / `DJC_DEPS_STRATEGY` with strategies `document`/`simple`/`prepend`/`append`/`raw` and a legacy `type=` alias | One `serialize(deps_strategy=..., deps_position=...)` call: `deps_strategy` is `document`/`simple`/`fragment`/`ignore`, `deps_position` is `smart`/`prepend`/`append` | Call `serialize()` on the render result and pass the strategy there: `MyComp(...).render().serialize(deps_strategy="document", deps_position="append")`. Map `prepend`/`append` to `deps_position`, map `raw` to `deps_strategy="ignore"`, and drop `type=`. A project-wide default goes on the `Citry(...)` instance rather than in settings. | 🟡 |
| <span id="djc-012">DJC-012</span> | Static asset delivery | Component assets are served through `ComponentsFileSystemFinder` / `collectstatic`, gated by `static_files_allowed` / `static_files_forbidden` | citry serves only generated component scripts/styles through its own mounted WSGI/ASGI routes; component source (`.py`/`.html`) is never served | Remove the finder from `STATICFILES_FINDERS`, drop `collectstatic` for components and the `static_files_*` settings, and mount citry's asset routes. A custom `media_class` (overridden `render_js`/`render_css`) has no hook either: control tag output with `Script`/`Style` entries and the `on_dependencies` hooks. | 🔴 |
| <span id="djc-045">DJC-045</span> | Order of inherited JS/CSS | A subclass's `Media` entries come before its parent's, so the parent's CSS wins equal-specificity ties | The parent's entries come first and the subclass's last, so the subclass's CSS wins the tie | Usually nothing: the new order is the one that lets a subclass override its parent's styles. If you relied on the parent winning, restate the parent's rule in the subclass. | 🟡 |
| <span id="djc-046">DJC-046</span> | Order of classes named in `extend` | The listed classes' assets merge in reverse order | They merge in the order you wrote them (`extend = [A, B]` gives A's assets before B's) | Only matters when two listed classes ship conflicting styles: if you relied on the reversed order, reverse your list. | 🟡 |
| <span id="djc-047">DJC-047</span> | `bytes` asset paths | A `bytes` path in `Media` is accepted | Raises `TypeError` naming the component and the offending value | Decode `bytes` paths to `str` (or use a `pathlib.Path`). The error tells you exactly which component and entry to fix. | 🟡 |
| <span id="djc-074">DJC-074</span> | Delivery of `js_data()` values to the browser | The script carrying `get_js_data()` values is generated, cached, and shipped whenever the component has any JS at all, even a plain script that never reads the data | The `js_data()` script reaches the page only when the component's JS registers a `$component` callback. With a plain script the data is never shipped, and nothing warns. (`css_data()` is unaffected: its stylesheet ships whenever the component has CSS) | JS that consumes `js_data()` values must read them inside a `$component` callback ([DJC-022](#djc-022) shows the shape). After porting, check every component that pairs a plain script with `js_data()`: either convert the script to the callback form or delete the unused `js_data()`. If a test only asserted the data script's presence, drop that assertion for plain-script components. | 🟡 |
| <span id="djc-085">DJC-085</span> | Browser dependency-manager namespace | The runtime exposes `DjangoComponents`, the legacy `Components` alias, `createComponentsManager()`, and `registerComponentData(..., factory)` | One load-safe singleton lives at `globalThis.Citry.manager`; there are no django-components aliases or manager factory, and `registerComponentData` takes the data value itself | Replace both old globals with `Citry.manager`, delete calls that construct private managers, and pass the JS-data object rather than a factory when registering data manually. | 🔴 |
| <span id="djc-086">DJC-086</span> | Component initialization completion and failures | `callComponent()` returns a Promise for the callback's synchronous or asynchronous result; callback errors reject it | `callComponent()` is synchronous fire-and-forget and returns `undefined`. A returned function is the instance cleanup; other values are ignored. Returned Promises are unsupported, and synchronous throws or Promise rejections are logged and isolated so later initialization continues | Move asynchronous server work to an Events handler and await the client `sendEvent()` promise. Keep `$component` initialization synchronous, return only an optional cleanup function, and do not await `callComponent()` or use callback return values as results. | 🔴 |
| <span id="djc-087">DJC-087</span> | Component initialization with no DOM roots | A component call rejects when no element carries its instance marker | The callback still runs with `els=[]`; rootless components and temporarily absent roots are valid lifecycle states | Handle an empty `els` array when initialization needs a root. Do not use rejection from `callComponent()` as a missing-root signal. | 🟡 |

## Extensions and lifecycle hooks

Move extension behavior to Citry lifecycle hooks, routes, configuration, and commands.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-050">DJC-050</span> | Render lifecycle hooks | Three hooks: `on_render_before`, `on_render` (with a lambda-yield protocol), `on_render_after` | One hook: `on_render(self)`. Return content (or `None` to keep the template), or write it as a generator: code before `yield` runs before the template renders, the `yield` receives the finished result, and code after it can inspect or replace the output | Merge the three bodies into one `on_render`: the before-hook code goes before the `yield`, the after-hook code after it. Each `yield content` replaces the output and receives the new result; errors arrive at the same `yield`. Note the yield hands back a render object, not a string: to append to the output, `return str(result) + "..."`. Code that added template variables in `on_render_before` moves into `template_data`. | 🔴 |
| <span id="djc-051">DJC-051</span> | Inputs named with a leading `@` | `@lol=2` arrives in the component's kwargs like any other input | An `@`-prefixed attribute with a string value is a client-side event instruction (for the events layer); it never reaches the component's inputs, and nothing warns you. A bare `@`-flag or a non-string value fails loudly with a `TypeError` naming the attribute | Rename data inputs that start with `@` (for example `at_lol` or `on_lol`). Audit templates for `@`-prefixed attributes that were meant as data, not events. | 🔴 |
| <span id="djc-052">DJC-052</span> | The render marker attribute | Each rendered root carries `data-djc-id-&lt;id&gt;` | Each rendered root carries `data-cid-&lt;id&gt;=""` (a fresh id per render) | Update CSS selectors, JS lookups, and snapshot assertions that match `data-djc-id-*`. | 🟡 |
| <span id="djc-055">DJC-055</span> | Registry lifecycle extension hooks | `on_registry_created` / `on_registry_deleted` fire when a standalone registry is constructed or collected | There is no standalone registry: it is part of each `Citry` engine, and no such hooks exist | Observe engine creation with `on_extension_created` (its context carries the engine); registry-deletion logic has nothing to attach to, since per-engine state dies with the engine. | 🟡 |
| <span id="djc-056">DJC-056</span> | `on_component_rendered` when a render fails | Fires once on the failing component itself, with the error message wrapped in the components-path prefix | The failing component's own hook does not fire; each *enclosing* component's hook fires as the error bubbles, receiving the original exception | Move per-component error handling (logging, boundaries) to an ancestor's hook or wrap the component; match the original exception, not the wrapped djc string. | 🟡 |
| <span id="djc-057">DJC-057</span> | Extension URL routes | Auto-served by Django under `/components/ext/&lt;name&gt;/`, Django path syntax with typed converters (`&lt;int:id&gt;` hands the handler an int) | A framework-neutral route table the host app mounts (via a `citry.contrib` adapter) under `&lt;prefix&gt;/ext/&lt;name&gt;/`; params are `{name}` segments, always captured as strings | Rewrite `&lt;int:id&gt;` as `{id}` and convert inside the handler (`int(id)`); return a `RouteResponse` instead of an `HttpResponse`; mount `Citry.urls` in the host app. | 🔴 |
| <span id="djc-058">DJC-058</span> | Declaring an extension's per-component config | A nested class named `ComponentConfig` (legacy alias `ExtensionClass` still accepted) | A `Config` class attribute subclassing `Extension.Config`; there is no legacy alias | Rename `ComponentConfig` (or `ExtensionClass`) to `Config` and its base to `Extension.Config`; update hook bodies for the renamed context fields (`ctx.component_class` and friends). | 🔴 |
| <span id="djc-059">DJC-059</span> | Reading hook-processed assets | `Component.template` / `.js` / `.css` return content with the loaded-hooks applied | The class attributes keep exactly what you wrote; the hook-processed, cached content comes from `get_template().source` / `get_js()` / `get_css()` | Switch introspection and tests that read the class attributes expecting processed content to the accessor methods. | 🟡 |
| <span id="djc-061">DJC-061</span> | Dropping a component class at runtime | Classes are not registered at definition, and the file index tracks them weakly, so an unregistered class dies with your last reference | Defining a class registers it, and the engine holds it strongly: call `engine.unregister(cls)` before dropping the last reference; render caches then release it normally | Unregister classes you replace at runtime (hot-swap tooling, plugin unload), then drop your own references. A fully rendered class is collectable and its weak file-index entry is pruned. | 🟡 |
| <span id="djc-084">DJC-084</span> | Extensions: authoring CLI commands | An extension declares CLI commands as `ComponentCommand` subclasses; the command's `handle` receives Django's global options (`settings`, `pythonpath`, `skip_checks`, ...) and underscore-prefixed parser internals in its kwargs, which authors had to pop out | The same declarative shape lives on citry's `ExtensionCommand` (imported from `citry`): `name`, `help`, arguments built from `CommandArg`/`CommandArgGroup`, nested subcommands, and a `handle(**kwargs)`. `handle` receives only the options the command tree declares, nothing needs popping, and the engine the CLI resolved is available as `self.citry`. Users run it as `citry ext run &lt;extension&gt; &lt;command&gt;` | Rebase command classes onto citry's `ExtensionCommand` and update the imports (`CommandArg`/`CommandArgGroup` keep their argparse-matching fields). Delete any code that pops parser internals or reads Django global options from kwargs; reach the engine through `self.citry` instead of Django settings. | 🔴 |
| <span id="djc-090">DJC-090</span> | Component-class deletion extension hook | `on_component_class_deleted(ctx)` receives `OnComponentClassDeletedContext` from a class finalizer | Citry exposes neither the hook nor the context because Python can run finalizers while arbitrary application locks are held | Move explicit-removal cleanup to `on_component_unregistered`. Use weak containers for memory-only indexes that should disappear with an unregistered class. `Citry.clear()` is a bulk teardown and emits no per-component hooks. | 🔴 |

## Setup, discovery, and command line

Connect tooling to the same Citry instance used by the application and remove Django-owned discovery settings.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-026">DJC-026</span> | Registry ownership and discovery | Standalone registries, `@register(..., registry=...)`, and global `all_registries()` support custom scopes and process-wide enumeration | Each `Citry` instance owns its registry; classes declare `citry = app` or use `app.register(...)`, and there is no global registry inventory | Create and retain a `Citry` instance for each component scope. Replace decorators with class assignment or `app.register`, and retain any registry/app references your own tooling needs instead of calling `all_registries()`. | 🔴 |
| <span id="djc-027">DJC-027</span> | Settings scope and component directories | Django's global `COMPONENTS` accepts a dict or `ComponentsSettings`; absent `dirs` defaults to `BASE_DIR/components` | Settings are typed and per `Citry` instance; component directories are explicit absolute `dirs` and no Django `BASE_DIR` is consulted | Move `COMPONENTS` values into `Citry(...)` arguments or `CitrySettings(...)`. Pass every component directory explicitly as an absolute path. | 🔴 |
| <span id="djc-028">DJC-028</span> | Parser-reserved component names | Protected names follow django-components' registered Django tags and selected formatter | `if`, `elif`, `else`, `for`, `empty`, `raw`, `fill`, and `slot` are citry's own tag names, so no component can use them | Rename a colliding component and update its `&lt;c-*&gt;` uses; for example, rename `Empty` to `EmptyState`. | 🔴 |
| <span id="djc-060">DJC-060</span> | Turning on hot reload | The `reload_on_file_change` setting (`True`/`False`/`"hot"`/`"restart"`/`"off"`) | An explicit call: `enable_hot_reload(engine, mode="hot")` (or `"restart"`); nothing watches until you call it, and there is no `off` value | Delete the setting; call `enable_hot_reload` where your dev server starts. An invalid mode fails at the call, not at settings load. | 🟡 |
| <span id="djc-064">DJC-064</span> | The `libraries` setting | `COMPONENTS["libraries"]` lists module paths that `import_libraries()` loads at startup | No such setting or helper; component modules are found by scanning `Citry(dirs=...)` or by ordinary imports | Delete the `libraries` entry: move those modules under a scanned directory, or import them plainly where your app starts. | 🔴 |
| <span id="djc-065">DJC-065</span> | Running autodiscovery | The module-level `autodiscover(map_module=...)` function, anchored to Django apps | An instance method: `app.autodiscover()` (or the default lazy scan on first lookup); no `map_module` hook; paths anchor to `sys.path` | Call `autodiscover()` on your `Citry` instance or rely on the lazy default; delete `map_module` usage. | 🟡 |
| <span id="djc-066">DJC-066</span> | The `@djc_test` testing harness | Wraps tests to reset djc's process-global state (registries, caches, `sys.modules` snapshots) | No harness ships. Each `Citry` instance owns its registry and caches; the one process-wide piece of state is the default instance, which components fall back to when they do not set `citry=` | Remove `@djc_test`; create a fresh `Citry()` per test and pass it to the components under test (`citry = c`) instead of relying on the default instance. | 🟡 |
| <span id="djc-077">DJC-077</span> | Reserved component names (built-in tags) | Two built-in names are taken at startup, `dynamic` (or your configured rename) and `error_fallback`; registering another component under either raises `AlreadyRegistered` | The built-in tag names `component`, `element`, `provide`, `cache`, `error-fallback`, `js`, and `css` are all reserved. Because a component class auto-registers under its lowercased class name, a class simply named `Element` fails at class definition with `AlreadyRegistered` naming the built-in it collides with | Rename a colliding class (for example `Element` to `ElementView`) or give it an explicit non-reserved `name` attribute, then update its `&lt;c-*&gt;` uses. Row #28 lists the parser tag names reserved for the same reason; this row adds the built-in component names. | 🔴 |
| <span id="djc-080">DJC-080</span> | Reading mapping and slot-data keys with a dot in expressions | The Django template dot resolves dict keys too: `{{ data.error }}` shows the `error` entry of a dict, and slot data is habitually read that way | Expressions use Python attribute access. Fill data is Citry's immutable `SlotData`, so identifier keys support `{{ d.error }}`; unusual keys and names colliding with mapping methods use brackets or fill destructuring. Ordinary dict values still require subscripts. | Keep dot access for identifier-like slot-data keys. Rewrite dot access only when the value is an ordinary dict, or use brackets/destructuring for an unusual slot-data key such as `aria-label`. Dot access on real object attributes is unchanged. | 🟡 |
| <span id="djc-081">DJC-081</span> | Running component commands | Component commands run through Django: `python manage.py components create\|upgrade\|ext\|list`, carrying Django's global options (`--settings`, `--pythonpath`, `--traceback`, `--no-color`, `--skip-checks`, `-v`) | Installing citry puts a standalone `citry` command on your PATH: `citry list`, `citry inspect [component] --json`, `citry create &lt;name&gt;`, `citry watch`, `citry ext list`, `citry ext run &lt;extension&gt; &lt;command&gt;`, plus `--version`. `inspect --json` emits the successfully loaded engine's versioned runtime component catalog; the optional case-insensitive name or alias keeps the same catalog envelope with one component. Neither form has a static-analysis fallback. There is no `manage.py` integration and Django's global options do not exist; a project that builds its own `Citry` instance points the CLI at it with a leading `--app module:attribute` (the same convention ASGI/WSGI servers use). The `upgrade` and `startcomponent` commands do not exist: `upgrade` migrated legacy Django-template syntax that citry does not use, and `startcomponent` was djc's deprecated alias of `create` | Replace every `manage.py components ...` invocation in scripts, docs, and CI with the `citry` binary; add `--app your.module:engine` as the first argument if your project constructs its own engine. Remove Django global options from those invocations. Anything that ran `upgrade` has nothing left to migrate; replace `startcomponent X` with `citry create X` ([DJC-082](#djc-082)). | 🔴 |
| <span id="djc-082">DJC-082</span> | The `create` scaffold | `components create X` scaffolds a directory `X/` with `template.html`, `script.js`, `style.css` (and `X.py`), customizable via `--js`/`--css`/`--template`, previewable with `--dry-run`, overwritable with `--force`, chatty with `--verbose` | `citry create MyButton` writes a single `my_button.py` containing the component class with an inline multiline template (no separate HTML/JS/CSS files), takes only `--path`, always prints the created file path, and refuses to touch an existing file; there is no `--force`, `--dry-run`, `--js`/`--css`/`--template`, or `--verbose` | Expect one Python file per scaffold instead of a directory of assets, and drop the removed flags from any wrapper scripts (they now fail with a usage error). To redo a scaffold, delete the file first; the command will never overwrite it for you. | 🟡 |
| <span id="djc-083">DJC-083</span> | Listing output and its flags | `components list` prints `full_name` and `path` columns (dotted class path plus source file), and `list` / `ext list` accept `--all`, `--columns`, and `--simple` to add columns, pick columns, or drop the header row | `citry list` prints one row per component: all its registered names (the lowercased and kebab-case forms share the row), the class name, and the file defining the component (relative to the working directory when inside it; a component with no source file leaves the cell empty); `citry ext list` prints the extension names. The columns are fixed and there are no `--all`/`--columns`/`--simple` flags (passing one is a usage error) | Update anything that parses the listing output to the new fixed columns; strip the formatting flags from saved invocations (they now fail with a usage error). | 🟡 |

## Component APIs, dynamic rendering, Events, and caching

Finish the Python-facing API changes, then verify dynamic components, HTTP behavior, and cached output.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-062">DJC-062</span> | Choosing a component by a variable | `{% component name_var %}` is rejected with "Component name must be a string 'literal', got: ...", steering you to other patterns | A tag name is always literal (`&lt;c-{{ name }}` will not interpolate); the dynamic path is the built-in dynamic component: `&lt;c-component c-is="name_var" /&gt;` | Rewrite variable-name calls as `&lt;c-component c-is="..." /&gt;`. | 🔴 |
| <span id="djc-067">DJC-067</span> | Data-method names and signatures | Template/JS/CSS data come from `get_template_data(self, args, kwargs, slots, context)` (and `get_js_data`, `get_css_data`) | The methods are `template_data(self, kwargs, slots)`, `js_data(self, kwargs, slots)`, `css_data(self, kwargs, slots)`. A ported component that still defines `get_template_data` renders without any error, but the method is never called: the template sees only the raw kwargs, so the output is silently wrong. | Rename the three methods and drop the `args` and `context` parameters (values read from the Django context become explicit props, `provide`/`inject`, or `template_globals`; see #8 and #20 for those parameters). After porting, grep the project for `def get_template_data`, `def get_js_data`, `def get_css_data`: any hit is a dead method. | 🔴 |
| <span id="djc-068">DJC-068</span> | What a bare typed-input class becomes | A bare inner `Kwargs`/`Slots`/`TemplateData` (etc.) class is rebuilt as a NamedTuple: instances are tuples, so `kwargs[0]`, `a, b = kwargs`, iteration, and `_asdict()`/`_replace()` all work | The same class is rebuilt as a dataclass with fixed attributes: attribute access works, tuple behavior does not (indexing, unpacking, and `_asdict()` raise), and setting an undeclared attribute on an instance also raises | Replace tuple-style access on typed instances with attribute access (or `self.raw_kwargs` for a plain dict). Classes declared with an explicit base (NamedTuple, `@dataclass`, pydantic model) are left untouched by both frameworks, so those need no change. | 🟡 |
| <span id="djc-069">DJC-069</span> | Declaring a no-inputs component | `Kwargs = Empty` (imported from django-components) declares the component takes no inputs; violations raise `TypeError` at render | There is no `Empty` type (the import itself fails). The same contract is an empty `class Kwargs: pass`: a template attribute then fails at parse ("can only have the following attributes ..."), and a Python-call kwarg raises `TypeError` at render | Replace `Kwargs = Empty` with `class Kwargs: pass`, and delete `Args = Empty` entirely (components are kwargs-only, [DJC-020](#djc-020)). | 🟡 |
| <span id="djc-070">DJC-070</span> | Files with dots in their names in component dirs | Dot-prefixed files and directories are silently skipped during discovery, but other dotted names (a `card.old.py` backup, an `assets.v2/` directory) crash the scan | Any file or directory with a dot in its name (beyond the `.py` suffix) is silently skipped: dot-prefixed junk (`.#card.py` editor locks, `._card.py` macOS copies, `.cache/` trees), backup copies like `card.old.py`, directories with a dotted name (hiding their whole subtree), and symlinks resolving to such paths | Files that crashed djc's scan are now skipped, and every regular file djc discovered is still discovered. One exception: a clean-named symlink pointing at a file inside a dot-prefixed directory was imported by djc but is skipped by citry; point the symlink at a dot-free path or replace it with the real file. If a skipped file should be discovered, rename it to a plain dot-free name. | 🟡 |
| <span id="djc-071">DJC-071</span> | Declaring default input values | Defaults live in a separate inner `Defaults` class; `Default(...)` wraps a factory for mutable values; `get_component_defaults(MyComponent)` reads the resolved defaults | Defaults are ordinary field defaults on the declared `Kwargs` class (`size: int = 10`); a factory is `dataclasses.field(default_factory=...)`; there is no defaults-reading helper. An inner class still named `Defaults` is silently ignored: nothing errors, the defaults just stop applying (a template reading the value fails with a missing-name error; if a `Kwargs` class is declared, passing the input is rejected as unexpected, and without one the input is simply accepted untyped) | Move each `Defaults` attribute onto the `Kwargs` class as an annotated field: `variable = "test"` becomes `variable: str = "test"`. The annotation is required, an unannotated `name = value` declares nothing. Rewrite `Default(fn)` as `field(default_factory=fn)`, and a mutable default like `items = []` as `field(default_factory=list)` (writing `items: list = []` fails at class definition with "mutable default ... use default_factory"). There is no direct replacement for `get_component_defaults(...)`. If the caller only inspects declarations, iterate `dataclasses.fields(MyComp.Kwargs)` and handle `dataclasses.MISSING` while reading each field's `default` or `default_factory`. If it needs resolved per-instance values, read them from the typed kwargs instance during rendering, or add an application helper that explicitly invokes factories. Then delete the `Defaults` class: leaving it behind fails silently. | 🔴 |
| <span id="djc-072">DJC-072</span> | Passing `None` to get the default | An input explicitly given as `None` still receives its declared default (`None` is treated as "missing") | `None` is a value like any other: the default applies only when the input is omitted, so a template that used to show the default now shows `None` | Omit the input where you meant "use the default". If a caller may legitimately hold `None`, resolve it yourself in `template_data` (`value if value is not None else fallback`). This changes output silently, so audit call sites that pass `None` on purpose. | 🟡 |
| <span id="djc-073">DJC-073</span> | Defaults in the raw kwargs dict | `self.raw_kwargs` includes the defaults for inputs the caller omitted | `self.raw_kwargs` holds exactly what the caller passed; defaults appear only on the typed kwargs (the `kwargs` argument of `template_data`). Reading an omitted input from the raw dict raises `KeyError` | Read defaulted inputs through the typed kwargs (`kwargs.size`), not the raw dict. Where code iterates `self.raw_kwargs` expecting the complete set of inputs, switch it to the typed instance. | 🟡 |
| <span id="djc-075">DJC-075</span> | Where processed component JS/CSS is cached | Processed JS/CSS is written to the Django cache named by the components `cache` setting (a private in-memory cache of its own when unset, never Django's default cache), under `__components:...` keys, as soon as the component class is defined | Each `Citry` instance writes to its own pluggable cache (`Citry(cache=...)`, a per-instance in-memory store by default), under `citry:...` keys, when the component first renders | Multi-worker setups that shared processed assets through a configured Django cache must pass a shared store to `Citry(cache=...)`; ready adapters exist for the Django cache framework (`citry.contrib.django.DjangoCache`), Redis, and diskcache (`citry.contrib.caches`). Update monitoring or warm-up jobs that looked for `__components:*` keys or expected the cache to fill at import time: keys start with `citry:` and appear at first render. | 🟡 |
| <span id="djc-076">DJC-076</span> | Dynamic components | The dynamic component is a Python class you can import and render directly (`DynamicComponent.render(kwargs={"is": ...})`), registered under the tag name `dynamic`, and renameable with the `dynamic_component_name` setting | The dynamic component is the fixed built-in `&lt;c-component&gt;` tag. There is no importable wrapper class and no rename setting; the tag name cannot be changed | Rewrite `{% component "dynamic" is=x %}` (and any renamed shorthand) as `&lt;c-component c-is="x" /&gt;` ([DJC-062](#djc-062) shows the invocation shape). Delete the `dynamic_component_name` setting. In Python, drop the `DynamicComponent` import and resolve the target yourself: `app.get(name)(**kwargs)` when you hold a name, or call the component class you already hold. | 🔴 |
| <span id="djc-079">DJC-079</span> | The built-in error boundary component | `{% component "error_fallback" %}` with the guarded content in a `content` slot (also fillable as `default`) and the fallback as a `fallback` slot or kwarg; the `ErrorFallback` class is importable; giving the fallback as both slot and kwarg raises `TemplateSyntaxError` | `&lt;c-error-fallback&gt;`: the guarded content is the tag body; the fallback is the `fallback="..."` attribute, or a `fallback` fill that receives the error as slot data (the guarded content then goes in the `default` fill, since fills cannot mix with other content). There is no importable class. A leftover `&lt;c-fill name="content"&gt;` fails on the component's first render, with a parse error naming the fill (the class itself defines without error). Giving both fallback forms raises `RuntimeError` ("give only one") | Rewrite the invocation as `&lt;c-error-fallback&gt;` with the guarded content directly in the body. When you use the fallback fill, rename the `content` fill to `default` and read the error as `d.error`. Delete `ErrorFallback` imports; Python-side, call `app.get("error-fallback")(fallback="...", slots={"default": ...})`. Update any except clause or test that matched `TemplateSyntaxError` or the old both-forms message. | 🔴 |
| <span id="djc-088">DJC-088</span> | Component HTTP handlers and their `self` | `Component.as_view()` dispatches `get` / `post` either from `Component.View` or directly from the component, and the handler can use a live component instance plus `render_to_response(context=..., slots=...)` | Put verb-shaped handlers in `class Events(ViewEvents):`. Their inputs are typed `data` and the neutral `request`; `self` is the per-call Events config, not a rendered component. Return a fresh component element or an Events action | Move each direct or nested view handler under `Events(ViewEvents)`, replace host request parsing with a data class, and replace `render_to_response` with a returned component or action. Move values formerly read from the live component into explicit data, context, State, or application services. | 🔴 |
| <span id="djc-089">DJC-089</span> | Component endpoint URLs and exposure | `get_component_url()` builds one optional public URL per component, `public=False` disables it, and `get_route_path()` plus `args` / `kwargs` define custom paths | Public methods placed in `Events` are exposed on fixed routes. Named handlers use `events.url(name, query=..., fragment=...)` or `get_event_url(...)`; there is no `public` flag or custom per-component route reversal, and the method-only ViewEvents route has no dedicated public builder | Replace public flags with handler placement or omission. Use named handlers and the event URL builders for durable call sites; keep query and fragment inputs, but move route parameters into typed event data. Treat `ViewEvents` as the initial method-shaped bridge, not a custom routing API. | 🔴 |
| <span id="djc-091">DJC-091</span> | Selecting a render-cache backend per component | `Component.Cache.cache_name` selects one named Django cache backend | A `Citry` instance owns one cache backend; component and fragment output caching use it | Pass the intended shared or local backend once as `Citry(cache=...)`. Split components across Citry instances only when they truly require different engine ownership; there is no per-component backend alias in V1. | 🟡 |
| <span id="djc-092">DJC-092</span> | Component cache key customization and Slots | `Cache.hash()` can replace key generation, while `include_slots` attempts to add Slot values automatically | `Cache.vary(self, kwargs, slots)` returns typed semantic variation and Citry owns canonical hashing. Every content-producing Slot requires an explicit variation; Citry never guesses from closures or source text | Replace `hash()` and `include_slots` with a `vary()` result containing only the values that can change output, including explicit Slot-presence or caller-controlled dimensions where relevant. | 🔴 |
| <span id="djc-093">DJC-093</span> | Template fragment cache syntax | Django's `{% cache timeout key *vary_on using=... %}...{% endcache %}` can run in a standalone Django template | Citry uses the transparent component `&lt;c-cache key="..." c-ttl="..." c-vary="..."&gt;...&lt;/c-cache&gt;` inside a component template, with the engine-owned backend | Move standalone cached markup into a root component template, translate timeout and variation to typed attributes, and remove `{% load cache %}` / `using=`. | 🔴 |
| <span id="djc-094">DJC-094</span> | IDs inside cached rendered output | Django's fragment cache reuses frozen rendered HTML, including the original component ID | Citry caches a detached artifact and mints fresh descendant IDs on every replay while reusing only the current boundary ID | Do not persist or compare a descendant `data-cid-*` across renders. Bind browser state to the current render; Citry remaps ownership, dependency, and Events records to those fresh IDs. | 🟡 |

## Update exact-output tests

These differences do not change the page a browser presents, but error assertions and exact HTML snapshots may need updating.

| ID | Area | django-components | Citry | What to change | Impact |
|---|---|---|---|---|---|
| <span id="djc-003">DJC-003</span> | Single-quote HTML escaping | Escaped as `&#x27;` | Escaped as `&#39;` (the same character, numeric-decimal entity) | Nothing for rendered pages; browsers treat the two entities identically. Update only tests that assert the literal `&#x27;` bytes. | 🟢 |
| <span id="djc-005">DJC-005</span> | Error when a component's inline JS/CSS contains its own end tag | Raises `RuntimeError`, message `...contains '&lt;/script&gt;' end tag.` | Raises `ValueError`, message `...contains a '&lt;/script&gt;' end tag. This is not allowed.` | If you catch this error or assert its message, switch to `ValueError` and the new wording. | 🟢 |
| <span id="djc-006">DJC-006</span> | The citry runtime script | A dependency-manager script is emitted on every document render | Citry adds its runtime script (`citry.js`) only when a page needs it: when a component uses `$component`, or when the page must stay in step with HTML fragments loaded later | Nothing changes in your templates. In tests, drop assertions that the runtime `&lt;script&gt;` is present on every rendered document. | 🟢 |
| <span id="djc-038">DJC-038</span> | Parentheses around expressions | Python-expression mode is opt-in per value: `disabled=(not editable)` | A `c-` value is always an expression, so the parentheses are not what makes it one. Keeping them still works | Nothing has to change. When tidying, drop them: `c-disabled="not editable"`. What matters is the `c-` prefix, not the parentheses. | 🟢 |
| <span id="djc-041">DJC-041</span> | Builtins in expressions | Helpers such as `len` are commonly added to the render context per call | Python builtins are not available inside expressions: `len(...)`, `str(...)` and friends raise `NameError` unless you supply the name yourself | Not a behavior change, but there is a better home for them: register helpers once with `Citry(template_globals={"len": len})` instead of passing them on every render. | 🟢 |
| <span id="djc-044">DJC-044</span> | Assets on a plain definition class | A non-component base class carrying a `Media` class contributes its entries | Reusable definition bases and plain classes named in `extend` contribute preserved `Dependencies`; relative paths resolve from the declaring module and files are registered to the consuming component | Keep reusable assets on the definition that owns them. Use `Dependencies = None`, `extend = False`, or an explicit `extend` list to cut or select branches. | 🟢 |
| <span id="djc-048">DJC-048</span> | Declaring one member of an asset pair as `None` | Setting `js = ...` while `js_file = None` (or the reverse) raises | Legal: only two values that are both set conflict; the set member is used | Nothing required. If you deleted an explicit `= None` to satisfy djc, you can put it back. | 🟢 |
| <span id="djc-049">DJC-049</span> | Protocol-relative asset URLs (`://example.com/x.js`) | The emitted tag escapes the leading colon (`href="%3A//example.com/..."`) | The entry is emitted exactly as written | Nothing for rendered pages. Update only tests that assert the escaped `%3A//` bytes. | 🟢 |
| <span id="djc-053">DJC-053</span> | Error paths for components placed via a fill | The error trace includes a slot segment for content rendered through a slot | Content failing inside a fill or fallback still shows a slot segment, as `Card(slot:body)` (djc wrote `provider(slot:content)`). Only a *component* placed via a fill loses the frame: its path is the authorship chain alone (`Page &gt; Failing`) | Update slot-marker assertions to the `Card(slot:body)` spelling, and drop the slot expectation only for component-failure paths. | 🟢 |
| <span id="djc-063">DJC-063</span> | Text next to explicit fills | Text or variables beside `{% fill %}` tags raise `TemplateSyntaxError` when fills are used | Same protection, different reporter: the parent's first render raises `SyntaxError`, worded "Text cannot appear next to '&lt;c-fill&gt;'" for literal text and "Expression cannot appear..." for a variable | Update assertions that match the djc error type or message. | 🟢 |
| <span id="djc-078">DJC-078</span> | Unknown component name error message | Rendering an unknown component name raises `NotRegistered` with the message "The component 'x' was not found" | Still `NotRegistered`, now worded "No component registered as 'x'." When it is `&lt;c-component&gt;` that cannot resolve the name, the message additionally suggests using `&lt;c-element&gt;` for a plain HTML element | Update except clauses and test assertions that match the old wording; the exception class name is unchanged. Nothing changes for code that only catches the exception type. | 🟢 |

## Migrate `Component.View`

If the project defines `Component.View`, finish the template and component
port first, then follow [Migrate from Component.View](/guides/migrate-from-component-view/).
It shows how to keep a verb-shaped route working before splitting it into
named, typed Citry Events.

## Verify the migration

Before removing django-components:

- Search again for its component, slot, fill, provide, dependency, and cache
  tags, plus its settings and registry imports.
- Run the original unit and snapshot tests, updating exact HTML assertions
  only where the checklist identifies an output-only difference.
- Exercise browser initialization, Alpine behavior, component assets, forms,
  CSRF protection, fragments, and event endpoints used by the migrated pages.
- Check that production serves Citry's generated asset routes and that no old
  component source directory is exposed as a static directory.
- Run `citry list` and `citry inspect --json` against the same `Citry` instance
  the application serves.

## Give this migration to a coding agent

Use the Markdown version of this page so the agent receives the checklist
without navigation or presentation markup. Start with an audit, review its
plan, and only then ask it to edit the project.


```text
Read this project's AGENTS.md, README, dependency files, and test commands.
Detect the installed Citry and django-components versions before choosing
documentation. Read https://citry.dev/llms.txt and use its version selector to
find the matching Markdown version of the "Migrate from django-components"
guide. If matching Citry documentation is unavailable, report the mismatch
instead of applying instructions from another version.

Audit this project for every DJC-### item in the migration guide. For each
applicable item, report the matching file paths, the required rewrite, and its
risk. Mark every other item not applicable or blocked. Include the existing
Python, browser, and snapshot commands that should verify the work.

Do not edit files yet. Produce a staged plan that migrates one connected group
of components at a time and keeps the application runnable between stages. Do
not add django-components compatibility shims.
```


After approving the audit, tell the agent which stage to implement. Require it
to report the `DJC-###` items addressed and the verification results before it
continues to the next stage.