Bluefly Drupal Component Doctrine — Props, Entities, Views, Dynamic Data & DRY Composition¶
Status: BINDING. Scope: every Bluefly Drupal site, Canvas implementation, SDC, Code Component, theme, component library, Recipe, and Drupal agent.
Third companion to Drupal AI Best Practices (AI/orchestration ladder) and Drupal Site-Building Doctrine (Canvas/component/theme ownership boundaries). That doctrine defines WHO owns a component; this one defines HOW a component must be built so it never needs a duplicate — the piece specifically targeting the CardA/CardB/ArticleCard/TeamCard one-off duplication pattern.
Core rule: BUILD COMPONENTS ONCE. CONFIGURE THEM WITH PROPS. COMPOSE THEM WITH SLOTS. CONNECT THEM TO DRUPAL DATA. DO NOT COPY COMPONENTS TO CHANGE CONTENT.
A component is a reusable rendering contract. It is NOT a page, customer copy, a node, a View, a hardcoded entity query, or a wrapper around one specific content item.
1. The model¶
Five layers: ENTITY (Drupal owns structured content/data) → VIEW/QUERY (Drupal owns collections/selection) → PROP (component receives typed data) → SLOT (component receives composed child components) → COMPONENT (renders reusable pattern) → CANVAS (connects pieces into pages).
Normal flow: Drupal Entity → Field/Entity Reference/View/Data Source → Canvas data binding → Component Props → Reusable Component → Canvas composition. NOT: Node → custom Twig → hardcoded markup → another custom Twig for the next node.
2. Props are the public API of a component¶
Props define what a component is allowed to know: title, description, eyebrow, image, link, variant, alignment, size, theme, date, author, icon, media, items, entityReference. Minimum information required to render — not articleNode/wholeEntity/rawDatabaseRow/everything when the component only needs title/summary/image/url.
3. Content must not be hardcoded into components¶
Bad: component Hero { return "Bluefly builds modern Drupal platforms"; }. Good: Hero({ title, description, image, primaryAction, variant }) — Canvas or Drupal supplies values. Same Hero reusable across homepage/service/campaign/customer/event/landing pages without source changes.
4. No component duplication for content variation¶
Don't create hero-home/hero-services/hero-about/hero-customer/hero-dark/hero-light/hero-image-left/hero-image-right unless genuinely different semantic components. Instead: hero with props variant/theme/mediaPosition/alignment. One component, many instances.
5. Variants are not new components¶
Visual variation should normally be a prop: variant(default/compact/featured/horizontal), theme(light/dark/brand), alignment(left/center), mediaPosition(left/right). Don't fork markup for minor presentation differences — use variants, keep the semantic contract stable.
6. No boolean prop explosions¶
Avoid isBlue/isBig/isCentered/isFeatured/hasBorder/darkBackground/useCompactMode/reverseImage — creates impossible combinations. Prefer intentional enums: variant(default|featured|compact), theme(light|dark|brand), alignment(start|center), mediaPosition(start|end). Props should constrain the design system, not expose every CSS decision.
7. Props must have types¶
Every prop needs a real schema via current Canvas/SDC metadata: string, formatted text, boolean, number, enum, link, image/media, entity reference, array/list, structured object where supported. No arbitrary untyped blobs for convenience. Canvas generates editing controls from these prop definitions.
8. Required vs optional¶
Only mark REQUIRED when the component cannot meaningfully render without it. Don't require presentation extras (Card: title required, url/summary/image/eyebrow optional). Components must handle optional values gracefully — no empty markup because an optional prop is missing.
9. Props vs slots¶
PROPS for data, SLOTS for composition. Card: props(title/description/image/link), slots(actions/metadata). Section: props(heading/width/theme), slots(content). Don't encode arbitrary child markup into string props when a slot is right. Canvas slots specifically accept other Canvas components including SDCs and Code Components.
10. Container components should use slots¶
A component organizing other components should expose slots, not know concrete children. Bad: FeatureGrid has feature1Title/feature1Text/feature2Title/feature2Text/feature3Title/feature3Text. Good: FeatureGrid props(columns/gap/theme), slot(items) — Canvas composes reusable Feature/Card components inside, grid doesn't care if there are 3, 4, or 8 items.
11. Do not model repeated items as numbered props¶
Never card1/card2/card3/card4, testimonialOne/testimonialTwo, link1/link2/link3 — immediate DRY violation. Use slot composition, a collection prop, a View, an entity reference list, or a Drupal data source depending on ownership.
12. Drupal entities are the data source¶
Reusable structured content (article, service, person, testimonial, case study, resource, event, media, taxonomy term) should live as Drupal entities. Don't duplicate entity content into Canvas props manually on every page. If content has an entity owner, reference it, don't copy it.
Facts about a business object are field values, not sentences in a body field. A number, a date, a client name, a technology, a metric, a status belongs in a typed field so that Views, components and agents can read it without parsing prose. A reusable factual claim that several pages cite (a result, a measurement, a proof) is its own entity referenced from those pages, never retyped. Prose that carries facts is a migration source for EXTRACT_INTO_STRUCTURED_FIELDS, not a home for them.
13. Connect components to entity fields¶
Canvas supports dynamic prop sources resolving entity field values. Article entity (title/field_summary/field_image/field_author/field_date) → ArticleCard props (title/summary/image/author/date/url). The component doesn't query the database — data binding maps Drupal data to the component API.
14. Components should not query their own entity¶
Bad: ArticleCard receives nid → loads node → reads fields → renders itself — tightly couples presentation to storage, causes unnecessary entity loading, hidden cache dependencies, poor preview behavior, difficult testing, reuse problems. Prefer: Drupal/Canvas resolves data → component receives values. Only use entity-reference objects when the component genuinely needs an entity-level concept.
15. Entity reference props are valid when the entity matters¶
Selected media item, selected taxonomy category, featured person, selected article, referenced product/service — legitimate entity-reference cases. Modern Canvas supports content-entity-reference prop shapes for Code Components with entity field expressions; current tooling can generate/inspect these — don't invent entity-reference schema syntax from memory, use current Canvas tooling/docs.
16. Entity references must not become hidden database coupling¶
Ask: does the component need (A) the entity identity, or (B) specific presentational values? If B, prefer mapped props. Bad: PersonCard(nodeId). Good: PersonCard(name, role, photo, bio, profileUrl) — unless actual behavior requires an entity reference.
17. Content templates are for entity→component mapping¶
When many entities of a bundle should render consistently, use a reusable content template rather than manually composing every entity. Article bundle → Canvas content template → Article layout (ArticleHero, ArticleMetadata, RichText, RelatedContent). Entity fields bind into component props — one template renders hundreds of entities. Don't construct each article page independently. Canvas content templates support static props, entity-field sources, and host-entity URL sources.
18. Static props vs dynamic props¶
Static: theme=dark. Dynamic: title=current entity.title. Static props describe presentation/configuration, dynamic props describe content/data. Don't duplicate dynamic content into static props when Drupal already owns the content.
19. Views own collection queries¶
"Show the latest six articles" is NOT six manually-maintained ArticleCard instances — it's a collection requirement. Use Views or another existing query/data-source layer. Drupal View → result entities/data → repeated Card component → Grid/List component. Don't hardcode content lists into page source.
20. Collection components must not own business queries¶
Bad: LatestArticles component hardcodes content type=article, status, sort, count=6, queries Drupal internally. Good: Collection/Grid component receives items and renders them; Drupal View/data source owns selection/filter/sort. Separates QUERY from PRESENTATION.
21. View=selection, component=presentation¶
Views answer "WHAT records?" (published articles, tagged resources, featured services, team members, latest case studies). Components answer "HOW should one result/collection look?" Don't duplicate View logic inside components or component markup inside View templates.
22. One Card, many collections¶
Same reusable Card for latest articles/related resources/featured services/search results/category archive through different data mappings. Don't create LatestArticleCard/RelatedArticleCard/SearchArticleCard/CategoryArticleCard unless actual semantic/presentation contracts differ. Use Card or ArticleCard plus variants.
23. Entity view modes are also reuse tools¶
Before inventing a new rendering path, evaluate Drupal view modes (full, teaser, card, featured, search_result) — a view mode can provide a stable entity presentation contract. Where Canvas/SDC integration makes sense, map entity/view mode to reusable components. Don't ignore core Drupal rendering abstractions merely because Canvas exists.
Every reusable business bundle SHOULD define a deliberate display-mode set — at minimum full, teaser, card, related, search_result; add featured and compact where composition needs them — and each mode MUST map to one controlled SDC or Drupal-native render pattern, preferably through a Canvas content template. Bundle-specific modes (for example a proof mode for a Work entity, a metric mode for an Evidence entity) are added by the product's content model, not invented per page. A View or a Canvas page that re-specifies presentation a display mode already owns is duplication.
24. Do not build a second Views system in JavaScript¶
Code Components can fetch data and Canvas provides SWR/Drupal-related packages — that doesn't mean every listing becomes a client-side JS query. Prefer Drupal-side Views/entity queries when data is Drupal-owned, SEO matters, caching matters, access checking matters, pagination matters, editorial configuration matters. Use client-side fetching only for a demonstrated interactive/external-data requirement — not because it's easier for the agent.
25. Don't fetch data that Drupal can already inject¶
Canvas can provide current page/site data to Code Components. Before fetch('/jsonapi/node/article/...'), ask: can Canvas/Drupal provide this through prop binding/content template/entity reference/page data/View? If yes, don't fetch it again — avoid duplicate network calls and client-side rehydration of Drupal-owned state.
26. Third-party API data is different¶
External data (weather, external search, GitLab status, Gas City status, remote API) may legitimately be fetched: API/data layer → normalized typed data → component props. Don't mix third-party fetch logic deeply into generic visual components — prefer a container/data component feeding a presentational component.
27. Presentational components should be pure when possible¶
Ideal: input props → deterministic render. Makes components testable, reusable, portable, previewable, cacheable, easier for Canvas editors. Avoid components whose output depends on hidden global state or undocumented services.
28. Never pass the entire Drupal world into a component¶
Avoid props like entity/node/context/siteConfig/everything/data unless genuinely required. Prefer explicit props — bad contracts become permanent coupling.
29. Field names should not leak into generic component APIs¶
Bad generic component: field_service_description/field_media_image/field_cta_url. Good: description/image/link. Drupal mapping translates field_service_description→description, field_media_image→image, field_cta_url→link. Allows the component to work across bundles/sites.
30. Components speak design language, not storage language¶
Component API: title/subtitle/image/actions/theme/variant. Drupal storage: field_headline/field_deck/field_media/field_links. Mapping occurs at the integration layer. Don't make Studio UI or shared components dependent on Drupal field machine names.
31. Entity bundles may differ — component contracts should not¶
Article uses field_summary, Resource uses field_description, CaseStudy uses field_intro — all three may feed Card.description. That's reuse. Don't create three components merely because field names differ.
32. Map many content types into one component contract¶
Example: Article(title→Card.title, field_summary→Card.description, field_image→Card.image, canonical URL→Card.link). Service and Resource map similarly to the same Card. One Card, three mappings, no duplicated markup.
33. Do not create a component for every content type¶
Content type ≠ component. Bundle is a content-model concept; component is a presentation concept — they may align sometimes, don't have to. Person entity can render as PersonCard/Avatar/AuthorByline/PersonProfileHero. Article can render as Card/SearchResult/FeaturedStory/ArticleHero. Don't enforce one bundle → one component.
34. Do not create a content type for every component¶
Inverse also wrong — Hero, Accordion, Grid, Section are not necessarily content types. These are presentation/composition concepts. Keep content model semantic, component model visual/compositional.
35. Reusable components should not own page position¶
A component shouldn't know "I am on the homepage" or "I am the third section." Canvas owns composition. Bad: if current_path == '/': make hero blue. Good: Hero(theme='brand'). Don't inspect route/page identity to determine generic component appearance.
36. Parent components must not know every child type¶
Use slots. Bad: Section has showCards/showTestimonials/showStats/showCTA. Good: Section props(theme/width/spacing), slot(content) — Canvas decides what goes inside. This is actual composition.
37. Component nesting should create a small vocabulary¶
Aim for a coherent design vocabulary (Page→Section→Heading+CardGrid→Card×N; Page→Section→Heading+TestimonialGrid→TestimonialCard). Don't build monolithic components (HomepageEverything.jsx) or hundreds of tiny meaningless ones.
38. Entity-reference props must use current Canvas mechanisms¶
Modern Canvas core supports content entity reference props for Code Components. Current local-codebase tooling uses dataDependencies.entityFields expressions; Canvas tooling can generate valid expression lists and preview resolved shapes. Don't guess expressions or hardcode internal entity shapes — use current CLI/context tooling. Workflow: refresh valid entity expressions → define component metadata → preview resolved prop shape → implement against observed shape → push → verify. Exact command syntax must come from current Canvas docs/tooling.
39. Contrib first for entity reference gaps¶
If core Canvas doesn't satisfy a particular entity-reference/editor UX need, search contrib before writing custom code. Example: Canvas Entity Reference provides declarative references to nodes/media/users/taxonomy with bundle restrictions/widgets/entity rendering, stable+security-covered as of 2026. Don't install automatically — evaluate whether native Canvas already solves it first. Contrib is a gap filler, not an automatic dependency.
40. Dynamic lists should be configurable, not code forks¶
"Show 3 featured resources" → dataSource=featured resources View, limit=3, display=cards, variant=featured. Don't create FeaturedResourcesThree.jsx then later FeaturedResourcesSix.jsx — component stays generic.
41. Filters belong with query configuration¶
Category filters, date range, publication status, sort order, result count, taxonomy conditions belong to the query/data-source layer, not hardcoded inside Card components. If editors should configure them, expose at the appropriate data-source configuration layer.
42. Pagination belongs to collection behavior¶
A Card doesn't implement pagination. A grid shouldn't secretly query page 2. Drupal Views/data source owns pagination. A higher-level collection component may render pager UI if that's part of the presentation contract — keep responsibilities separate.
43. Empty states are part of collection design¶
Dynamic components must handle 0 results, 1 result, many results, missing optional media/summary, unpublished/inaccessible referenced entity. Don't assume demo content always exists — test against missing data.
44. Access control must remain Drupal's job¶
Don't bypass Drupal access by fetching/rendering entities directly without proper access handling. If Drupal/Views/Canvas resolves data, preserve its access semantics. Don't create client-side APIs leaking unpublished/restricted content. Presentation must not become an authorization layer.
45. Cacheability must survive reuse¶
Dynamic Drupal rendering must preserve cache tags/contexts/max-age where applicable. Don't replace cache-aware Drupal rendering with manual raw database/API calls without understanding caching consequences. A visually correct component with broken cache invalidation is defective.
46. Use Views for cross-entity collections¶
Typical View-backed cases: Latest Articles, Related Resources, Team Directory, Services Index, Case Studies, Upcoming Events, Search Results, Featured Content. View owns filtering/sorting/access/pagination/selection. Components own presentation.
Related-content collections are derived from entity references (reverse references, contextual filters), not curated by hand. If a "related" block requires an editor to pick cards on each page, the relationship is missing from the content model, not from Canvas.
47. Do not build one View per page when one configurable View works¶
DRY applies to Views too. Before creating another View, can an existing display/configuration/filter/argument/contextual filter solve it? Don't proliferate nearly-identical Views for tiny differences.
48. Taxonomy is data, not a style prop¶
If "category" is a real domain concept, use taxonomy/entity reference. If "blue" is a style, use theme/variant prop. Don't turn domain classification into presentation flags, or presentation variants into taxonomy unless editors genuinely need domain classification.
Taxonomy classifies; it does not replace an entity that needs a lifecycle, fields, relationships or a full page of its own. Prefer shared vocabularies across bundles. Do not create a vocabulary to serve one page or one View.
49. Links should be real link contracts¶
Avoid buttonText/buttonUrl/buttonTarget/buttonRel scattered across unrelated props when the component system supports a structured link concept. Use the supported Canvas/SDC link shape — one semantic prop (primaryAction or link) with expected structure. Don't reinvent a link API in every component.
50. Media should be an owned data type¶
Don't create imageUrl/imageAlt/imageWidth/imageHeight/imageFocalX/imageFocalY unless that truly matches the supported component contract. Prefer Drupal/Canvas media/image prop types and existing transforms. Let Drupal media own file/alt text/metadata/focal point/reuse — don't copy media details manually into dozens of Canvas instances.
51. Repeated data should have one source¶
Company phone number in 20 components = wrong. Same exec bio on 5 pages = wrong. Same testimonial copy on 8 Canvas pages = wrong. Create/reference one Drupal-owned source, bind/render wherever needed. Do not duplicate content to achieve reuse.
After content is migrated into Drupal, published Drupal content is the only authority for website content. Planning documents describe intent; a Markdown, spreadsheet or Canvas copy of migrated content kept "for reference" is a second source and must be deleted or reduced to a pointer.
52. Entity references beat copy/paste for shared content¶
For reused content: entity → reference → component. Not: copy content → paste into Canvas → copy again next month. Use references for reusable semantic content, static Canvas props for truly page-specific content.
53. Page-specific content can remain page-specific¶
Not everything needs an entity — don't overmodel. A one-off page heading may simply be a prop. Create an entity when content has independent identity, reuse, workflow, ownership, searchability, relationships, lifecycle, translation, metadata. Don't create a "Heading" content type just because a Heading component exists.
54. Entity-first does not mean everything is an entity¶
Use judgment. Component instance values fit: layout variants, local headings, decorative settings, page-specific copy, presentation configuration. Entities fit: reusable domain content, structured records, content with lifecycle/workflow, relational data. Canvas sits between these worlds.
55. Content template vs page composition¶
Content templates: many entities of one bundle need the same presentation model. Canvas page composition: an editor is assembling a specific page. Don't manually recreate a content template on every entity. Don't turn every marketing landing page into a rigid entity template if editors require composition freedom.
56. Global regions are reusable composition¶
Header/footer/global structures shouldn't be copied into every Canvas page. Use current Canvas global-region mechanisms — defines an element tree once, shares that composition. Don't manually duplicate headers/footers across pages.
57. Pages should reference component types, not copy their source¶
Canvas page definitions compose component instances and their props/slots — should not embed another copy of component implementation source. Source belongs to the shared component owner; instance belongs to the Canvas page/template. Keep them separate.
58. Design system tokens are props only when editor choice is intended¶
Don't expose every design token as an editor control. Ask: should an editor actually choose this? If no, component/design system owns it. If yes, expose a constrained semantic prop. Bad: paddingTopPx=37. Good: spacing=compact|normal|spacious.
59. Component APIs should remain small¶
A component with 40 props is probably multiple components collapsed together, exposing implementation details, or modeling page composition instead of component behavior. Split by meaningful responsibility — don't solve every requirement by adding another prop.
60. Don't create "universal component" monsters¶
DRY does not mean one component with 100 modes. Bad: UniversalContentComponent variant=hero|card|testimonial|accordion|tabs|.... DRY means sharing genuine abstractions — separate concepts remain separate. Hero≠Card. Card variants share Card. Use semantic boundaries.
61. Reuse through composition before configuration explosion¶
When a component gains too many optional regions, consider slots/composition. Instead of Hero with showBadge/showStats/showForm/showLogos/showTestimonials/showButtons, use Hero with slots(eyebrow/content/actions/media) and compose children. Composition often scales better than boolean configuration.
62. Components must be data-source agnostic when possible¶
A generic Card shouldn't care whether data came from node/taxonomy/external API/View/static Canvas prop — it receives its contract. This is what allows reuse. The adapter/data binding owns the source.
63. Data adapters are allowed — duplicated components are not¶
Sometimes data shapes differ — create a mapping/adapter at the integration layer (Drupal Article, Drupal Service, External Result → normalized Card props → Card). Don't fork Card three times because sources differ.
64. Normalize at the boundary¶
Convert source-specific shapes before they reach the component. field_media.entity.field_media_image... shouldn't leak throughout component source — normalize to image at the integration boundary. Components consume stable contracts.
65. Component contract changes are API changes¶
Changing a reusable prop name/shape may affect Canvas instances, content templates, pages, global regions, other sites, Studio UI consumers, tests. Treat component API changes intentionally, prefer backward-compatible evolution, don't casually rename props across shared libraries.
66. Delete duplicates after convergence¶
If several components are discovered to be the same concept: identify canonical contract → add required variants → migrate consumers → verify → delete duplicate components. Don't leave aliases/compatibility copies forever unless required. DRY means reducing ownership.
67. Agent duplication check¶
Before creating any component, search Studio UI, shared component library, current theme, Canvas Code Components, SDCs, enabled contrib, other canonical Bluefly component sources. Return:
COMPONENT_REQUEST=
EXISTING_MATCHES=
CLOSEST_MATCH=
REUSE=
EXTEND=
NEW_REQUIRED=
68. Agent prop check¶
Before adding a prop:
WHAT_BEHAVIOR_DATA_DOES_THIS_REPRESENT=
EDITOR_SHOULD_CONTROL=<YES|NO>
CAN_EXISTING_PROP_EXPRESS_IT=<YES|NO>
CAN_SLOT_EXPRESS_IT=<YES|NO>
IS_THIS_A_DESIGN_TOKEN_DETAIL=<YES|NO>
IS_THIS_SOURCE_SPECIFIC_FIELD_LEAKAGE=<YES|NO>
69. Agent entity check¶
Before introducing duplicated content:
CONTENT_HAS_EXISTING_ENTITY=<YES|NO>
CONTENT_WILL_BE_REUSED=<YES|NO>
CONTENT_HAS_LIFECYCLE=<YES|NO>
CONTENT_NEEDS_WORKFLOW=<YES|NO>
CONTENT_NEEDS_RELATIONSHIPS=<YES|NO>
70. Agent collection check¶
Before manually placing multiple repeated components:
IS_THIS_A_COLLECTION=<YES|NO>
IS_SELECTION_RULE_BASED=<YES|NO>
SHOULD_IT_UPDATE_AUTOMATICALLY=<YES|NO>
71-77. Worked examples¶
Bad homepage: HomepageHero/HomepageServices/HomepageLatestArticles/HomepageTestimonials/HomepageCTA, each hardcoded with direct entity queries — creates a homepage application instead of a CMS.
Good homepage: Canvas Page "Home" with generic Hero(props), Section(heading="Services", slot=ServiceGrid[source=featured_services]), Section(heading="Latest insights", slot=CardGrid[source=latest_articles]), Section(slot=Testimonial[referenced entity=testimonial:123]), CTA(props). Components generic, data owned, composition site-specific.
Article system: Article entity owns title/author/date/hero media/summary/body/topics. One Canvas content template owns ArticleHero/ArticleMetadata/ArticleBody/RelatedContent, field bindings feed props dynamically — one implementation renders every article, no one-off templates.
Services: Service entity owns semantic content. ServiceCard accepts title/summary/icon/url. A View selects featured services. Canvas composes CardGrid←ServiceCard results. Homepage doesn't copy service descriptions — updating a Service updates every representation.
People: Person entity owns name/role/photo/bio/links. PersonCard/AuthorByline/PersonHero all consume mappings from the same Person entity — don't duplicate person content into each component.
Testimonials: Testimonial entity owns quote/attribution/organization/photo. Testimonial or TestimonialCompact component. Pages reference testimonials — don't paste the quote everywhere.
Related content: selection belongs to View/entity relationship/taxonomy query/recommendation service. Presentation belongs to RelatedContent/CardGrid/Card. Don't make Card discover its own related entities.
78-80. Demo data and content-length discipline¶
Static demo/example data is developer preview, not production content — don't ship sample strings as live content because the preview looked good. Component previews must use representative data: long titles, short titles, missing images, multiple links, empty optional fields, realistic text lengths — not just "Hello world" as proof of production-readiness. If a component can't render variable data lengths without breaking, fix the component contract/design — don't clamp everything arbitrarily because one test string was too long; content is dynamic, design for it.
81-83. Translation, preview/live parity, declared dependencies¶
Props bound to translatable Drupal fields stay in Drupal's translation model — don't copy translated content into static component source or make shared component code language-specific unless required. Preview and live must use the same contract — no fake preview-only adapter hacks; if entity-reference props resolve to structured objects, inspect the real resolved shape (current Canvas tooling has preview helpers for this) and code to the supported contract. Data dependencies must be declared explicitly through supported metadata/data-dependency mechanisms — no hidden reaches into global Drupal state; explicit dependencies keep components portable and analyzable.
84. No field-machine-name sprawl in shared source¶
Site-specific mappings may know field_featured_image; shared component code should know image. Critical for Studio UI reuse across multiple Drupal sites.
85-87. One owner per pattern¶
Views output should not duplicate component markup — don't write a custom View Twig template reproducing Card markup, make View results feed the reusable presentation mechanism; no parallel views-view-fields--articles.html.twig and card.twig implementing the same card. One owner for every visual pattern (CARD_SOURCE_OWNER=ONE_REPOSITORY, every other usage consumes it — no duplicate View/Canvas/theme/homepage/React card markup unless an intentional adapter with one shared design owner). One owner for every query too — don't reproduce "latest published articles" selection independently in a View, custom PHP, a JSON:API URL, and a React fetch; choose the proper query owner and reuse it.
88-90. Studio UI coupling and duplication measurement¶
Don't couple Studio UI/shared design components to Drupal entities — correct: Drupal entity → Drupal/Canvas adapter → Studio UI props; wrong: Studio UI importing Drupal-specific field/entity behavior — keep shared UI portable. Site components may adapt (a thin DrupalMediaCard wrapper delegating to StudioUI Card), never fork (copying Card source and modifying it). Agents should actively flag duplication: same markup in multiple components, same prop set with different names, same query repeated, same content repeated, same CSS repeated, same entity mapping repeated — possible actions CONSOLIDATE_COMPONENT / CREATE_VARIANT / CREATE_SLOT / CREATE_SHARED_ADAPTER / CREATE_VIEW / CREATE_CONTENT_TEMPLATE / REFERENCE_ENTITY / DELETE_DUPLICATE. Don't normalize duplication as "that's how the site evolved."
91. Final DRY acceptance¶
Before declaring a Drupal page/system complete, all must hold: DUPLICATED_CONTENT=0 where shared identity is intended, DUPLICATED_COMPONENT_IMPLEMENTATIONS=0, DUPLICATED_QUERY_LOGIC=0, DUPLICATED_DESIGN_PRIMITIVES=0, FIELD_NAMES_LEAKED_INTO_SHARED_UI=0, ENTITY_CONTENT_REFERENCED=YES, COLLECTIONS_DYNAMIC_WHERE_APPROPRIATE=YES, PROPS_TYPED=YES, PROPS_MINIMAL=YES, SLOTS_USED_FOR_COMPOSITION=YES, VARIANTS_USED_INSTEAD_OF_FORKS=YES, CANVAS_OWNS_COMPOSITION=YES, DRUPAL_OWNS_CONTENT=YES, VIEWS_DATA_LAYER_OWNS_SELECTION=YES, COMPONENT_OWNS_PRESENTATION=YES. If not, it is not DRY.
92. Final law¶
ONE CONTENT SOURCE. ONE QUERY OWNER. ONE COMPONENT OWNER. MANY USES. Flow: ENTITY → DATA SOURCE → PROP → COMPONENT → CANVAS. Not: COPY → PASTE → TWEAK → FORK → REPEAT. If data already exists, reference it. If a component already exists, configure it. If a query already exists, reuse it. If variation is presentational, use a prop. If child content varies, use a slot. If many entities follow the same presentation, use a content template. If a collection is rule-based, use Views/the appropriate data source. If the component is generic, keep Drupal field names out of it. Do not repeat.
Architecture summary¶
Drupal entities (Article/Service/Person/Resource/Testimonial) → Drupal data layer (field bindings, entity references, Views, content templates, page/site data) → normalized props → Card/Hero (props) and Section/Grid (props+slots) → Canvas → Page.
Core conceptual rule: entities are not components, and components are not entities — props are the contract between them. A Service entity, Article entity, and Resource entity can all map into the same Card contract even with different Drupal field names; the shared component never cares that one site calls something field_summary and another calls it field_deck — the integration layer normalizes both to description.
Practical rule: build the data model once, build the visual component once, build the mapping once, let Canvas compose it everywhere.