# HOMEPAGE_DEPENDENCY_INVENTORY.md

**Every dependency D10 needs to reproduce the D7 homepage, measured against what D10 has today.**

Investigated 2026-08-25 from the D7 database and codebase. **No D7 file, schema or row was
modified.** All D7 figures come from SELECT-only queries through `d10_migration_reader`; scripts
retained as `.logs/task26-d7-homepage-inventory.sh`, `task26b-d7-gaps.sh`, `task26c-states.sh`.

Companion to `HOMEPAGE_FLOW.md`, which describes the *routing*. This file is the *bill of
materials*.

---

## 0. 🛑 CORRECTION TO `HOMEPAGE_FLOW.md` — the homepage has FIVE panes, not four

`HOMEPAGE_FLOW.md` §1, `HomepageController`'s docblock and acceptance criterion §5 all describe
**four** panes. The D7 database has **five**. `panels_pane` for display `did=11`:

| position | pid | type | subtype | in the docs? |
|---|---|---|---|---|
| 0 | 28 | `node` | node 29, view mode `homepage_video` | ✅ |
| 1 | 29 | `block` | `mailchimp_signup-3_tips_to_increase_client_retent` | ✅ |
| **2** | **30** | **`custom`** | **inline `full_html` — testimonials + CTA** | 🛑 **ABSENT** |
| 3 | 31 | `panels_mini` | `education_provider_block` | ✅ (documented as position 2) |
| 4 | 32 | `panels_mini` | `information_blocks` | ✅ (documented as position 3) |

The earlier investigation read the panes of `did=8` — the *handler's* id — instead of `did=11`, the
id stored inside the handler's `conf`. The two ids differ, both exist, and both carry panes, so the
wrong query returned plausible rows rather than an error.

**Consequence.** `HomepageController::build()` emits four slots. As written, the D10 homepage would
render **without the testimonials block**, in the wrong order relative to it — silently, since a
missing pane raises nothing.

**The content is preserved.** Task 25 extracted it byte-exactly before anything could overwrite it:

```
.logs/extracted/homepage-custom-pane-pid30.html        540 bytes
.logs/extracted/homepage-custom-pane-pid30.meta.json   title/format/region/position
sha256  2899eea9e0d405163b29aee25f1f59310597ab325d522009dab30fffab51e4af
```

It exists **only** inside `panels_pane.configuration` — it is not a node, not a block, not a bean.
No migration in the 45-migration list reads `panels_pane`. Had the Panels tables been dropped, this
content would have been unrecoverable.

**→ Open decision `H-1` (§9). Not implemented here: how the HTML is stored in D10 is an
architectural choice, and the acceptance criterion in `HOMEPAGE_FLOW.md` §5 says "four components"
and would have to change.**

---

## 1. The six nodes — verified in D7

All six: published, single revision, authored by `admin`, language `und`, `promote=0`.

| nid | bundle | title | alias | body format | body bytes |
|---|---|---|---|---|---|
| 22 | `article` | Set Your Own Schedule | `content/set-your-own-schedule` | `full_html` | 152 |
| 23 | `article` | Expand Your Knowledge | `content/expand-your-knowledge` | `full_html` | 164 |
| 24 | `article` | Study From Anywhere | `content/study-anywhere` | `full_html` | 157 |
| 25 | `article` | Lower Your Education Expenses | `content/lower-your-education-expenses` | `full_html` | 161 |
| 28 | `article` | Your State Approved Online Continuing Education Provider | `content/your-state-approved-online-continuing-education-provider` | `filtered_html` | 360 |
| 29 | `video` | Get your Cosmetology CEU | `videos/get-your-cosmetology-ceu` | `full_html` | 148 |

Notes that matter for the migration:

- **Node 28 is `filtered_html`, the other five are `full_html`.** The five article bodies each open
  with an `<h2>`; under `filtered_html` D7 would strip nothing here, but the text-format mapping
  must not collapse the two formats into one.
- **`promote = 0` on all six.** They never appear in a default front-page listing — further
  confirmation the homepage is composed, not queried.
- **All six carry a URL alias.** `ce_url_alias` (migration 39) must run, or the aliases are lost.
- **`field_image` is empty for every one of them.** The articles are text-only; no image
  dependency.

---

## 2. Content types and fields required

| bundle | field | type | module | needed by |
|---|---|---|---|---|
| `article` | `body` | `text_with_summary` | text | nodes 22,23,24,25,28 |
| `article` | `field_image` | `image` | image | *(unused by these nodes)* |
| `article` | `field_tags` | `taxonomy_term_reference` | taxonomy | *(unused by these nodes)* |
| `video` | `body` | `text_with_summary` | text | node 29 |
| `video` | `field_instructor_s_` | `text` | text | node 29 — **empty** |
| `video` | `field_price` | `number_decimal` | number | node 29 — **empty** |
| `video` | `field_video_from_wistia_video` | `media` | media | node 29 — **populated** |

Node 29's Wistia reference:

```
field_video_from_wistia_video_fid = 2452
file_managed 2452:
  filename  Website Intro New-HD 1080p
  uri       wistia://v/42lx1o2wez        <- custom stream wrapper
  filemime  video/wistia
  filesize  0                            <- remote asset, no local bytes
```

The `wistia://` scheme is provided by D7 `wistia` + **`media_wistia_ceonline`** (a custom module;
contrib `media_wistia` is *disabled*). D10 side: **`ce_wistia` exists and is enabled**, and
migrations `ce_wistia_media` (44) and `ce_wistia_media_track` (45) exist. Node 29's hero video
depends on both landing correctly — `filesize=0` means there is no file to copy, only an id to
preserve.

---

## 3. The `homepage_video` view mode

**It is real, and it is bundle-specific.** `field_bundle_settings_node__video` defines seven view
modes on `node:video`:

```
full  teaser  rss  search_index  search_result  token  homepage_video
```

`homepage_video` has `custom_settings = TRUE` — it overrides the default display rather than
inheriting it. Its per-field settings:

| field | formatter | weight | label |
|---|---|---|---|
| `field_instructor_s_` | `text_default` | 0 | inline |
| `field_video_from_wistia_video` | `media` | 1 | **hidden** |
| `field_price` | `number_decimal` | 3 | inline |
| `body` | `text_default` | 4 | **hidden** |

Two things follow. First, weight 2 is unassigned — nothing sits between the video and the price.
Second, `field_instructor_s_` and `field_price` are configured to display **inline with labels**,
but node 29 has **no value in either**, so neither renders. The view mode is shared with other
video nodes where they do render.

The view mode is defined by the **`entity_view_mode`** module (enabled in D7). There is no
`entity_view_mode` table and no `entity_view_mode_settings` variable — so the mode is registered
in code, and the *per-field display* lives in `field_config_instance.data`. D10 needs
`core.entity_view_display.node.video.homepage_video` created to match; no migration in the
45-migration list creates view modes.

---

## 4. `information_blocks` — the one mini-panel that IS in the database

```
panels_mini pid=1  name=information_blocks  did=12  layout=onecol  hide_title=1
  region "middle":
    pane 33  node 25  full   override_title=1, title text ""
    pane 34  node 24  full   override_title=1, title text ""
    pane 35  node 22  full   override_title=1, title text ""
    pane 36  node 23  full   override_title=1, title text ""
```

**Order is 25, 24, 22, 23 — not numeric.** `HomepageController::INFORMATION_BLOCK_NODE_IDS`
matches, with a "do not sort" comment. ✅ correct.

All four suppress the pane title (`override_title=1`, empty text) — the `<h2>` in each node's body
supplies the visible heading instead. A D10 rebuild that renders node titles would produce
**duplicate headings**.

`responsive.css` targets `div#mini-panel-information_blocks` by id; the controller emits it. ✅

---

## 5. `education_provider_block` — code-only

`SELECT COUNT(*) FROM panels_mini WHERE name='education_provider_block'` → **0 rows.** It exists
solely as a Features export:

```
sites/all/modules/course_features/homepage/homepage.panels_default.inc
  $mini->name        = 'education_provider_block'
  $display->layout   = 'flexible'          <- NOT onecol
  region "center":
    node 28, view mode full
    block views-states_block-block
```

Its sibling `information_blocks` *is* in the database. So the two mini-panels have **different
storage** — one reverted to code, one overridden in the DB. Any migration reading only
`panels_mini` sees one of the two.

The `flexible` layout carries `layout_settings = array()` — empty, i.e. it is the default flexible
layout with a single `center` region and no custom column splits. Nothing structural is lost by
rendering it as one column, which is what the controller does.

---

## 6. `states_block` — code-only, and taxonomy-dependent

`SELECT COUNT(*) FROM views_view WHERE name='states_block'` → **0 rows.** Defined in
`sites/all/modules/course_features/ce_views/ce_views.views_default.inc` (module `ce_views`,
enabled).

```
base_table   taxonomy_term_data
displays     Master (default) + States (block)
field        taxonomy_term_data.name
               label ''          link_to_taxonomy TRUE
               make_link TRUE    replace_spaces TRUE   path_case lower
               link_class 'ce-[name]'          <- per-term CSS class
filters      taxonomy_vocabulary.machine_name = 'state'
             field_data_field_publish.field_publish_value = 1
pager        none        row plugin  fields        access  perm
```

**Its real dependency is taxonomy, and that taxonomy is filtered:**

| | D7 |
|---|---|
| vocabulary `state` | vid 4, "State" |
| total terms | **8** |
| terms with `field_publish = 1` | **4** — Georgia, North Carolina, South Carolina, Washington DC |

So the block renders **4 of 8** states. A D10 rebuild that omits the `field_publish` filter would
show **twice as many states as D7 does**, advertising approval in states the business has not
published. `field_publish` is a field on the **taxonomy term**, not the node — term fields are
easy to miss in a migration.

`ce_taxonomy_vocabulary` (38) and `ce_taxonomy_term` (37) exist. Whether they carry term-level
field data is **not verified here** and should be checked before this block is rebuilt.

Block placement in D7: `views:states_block-block` is present in all four themes with
`status = 0`, region `-1`. It is **not placed in any region** — it is rendered *only* as a Panels
pane. Correct: a D10 rebuild must not place it as a block.

---

## 7. Mailchimp signup

D7 has **two** signup forms; the homepage uses the second:

| mcs_id | name | title | mode | status |
|---|---|---|---|---|
| 1 | `newsletter` | Join Our Newsletter! | 1 | 1 |
| 2 | **`3_tips_to_increase_client_retent`** | 3 Tips to Increase Client Retent | 1 | 1 |

`3_tips_to_increase_client_retent` settings:

```
mc_lists       8006301bcc            (one list; the same list as `newsletter`)
submit_button  "Get it Now"
description    <h4>Send me the 3 easy steps to increase my client retention and other updates.</h4>
confirmation   "You have been successfully subscribed."
mergefields    EMAIL required (size 25); 13 MMERGE* fields all disabled (0)
doublein       0        placeholder 0        gdpr_consent 0
```

Only `EMAIL` is active — every `MMERGE` field is off. The form is one email input plus a button.

Block placement mirrors `states_block`: `mailchimp_signup:3_tips_to_increase_client_retent` is
`status=0`, region `-1` in all four themes — a pane, never a placed block. (The *other* form,
`newsletter`, **is** placed: `cetc_new` content region weight -19, `cetc` footer_bottom.)

**API config:** D7 holds `mailchimp_api_key`, `mailchimp_batch_limit`, `mailchimp_cron` as
variables. The key is a **live third-party credential** — it is not printed anywhere in this
inventory or its scripts, and it is not migrated automatically. Restoring the block at Phase 3.3
requires that key to be placed in D10 config deliberately, by someone authorised to handle it.

---

## 8. D7 → D10 comparison — where each dependency stands

| # | Dependency | D7 | D10 today | Verdict |
|---|---|---|---|---|
| 1 | nodes 22,23,24,25,28,29 | present, published | **0 nodes exist** | 🛑 blocked — `ce_node` (6) |
| 2 | bundle `article` | body + image + tags | **no content types at all** | 🛑 blocked |
| 3 | bundle `video` | body + instructor + price + wistia media | **no content types at all** | 🛑 blocked |
| 4 | view mode `node.video.homepage_video` | defined, `custom_settings=TRUE` | **absent** — only full/rss/search_index/search_result/teaser | 🛑 **no migration creates it** |
| 5 | URL aliases for all six | present | 0 aliases | 🛑 blocked — `ce_url_alias` (39) |
| 6 | Wistia file 2452 `wistia://v/42lx1o2wez` | present | `ce_wistia` enabled, 0 media | 🛑 blocked — `ce_wistia_media` (44) |
| 7 | mini `information_blocks` | DB, did=12 | reproduced in controller, id emitted | ✅ structure done |
| 8 | mini `education_provider_block` | code export | reproduced in controller, id emitted | ✅ structure done |
| 9 | view `states_block` | code export | **`views` enabled, view absent** | 🛑 not migrated |
| 10 | vocabulary `state` + `field_publish` | 8 terms, 4 published | **0 vocabularies, 0 terms** | 🛑 blocked — (37),(38) |
| 11 | Mailchimp block | `mailchimp_signup` enabled | **`drupal/mailchimp` NOT INSTALLED** | 🛑 deferred to Phase 3.3 |
| 12 | **custom pane, 540 bytes HTML** | `panels_pane` pid 30 | **not rendered at all** | 🛑 **decision H-1** |
| 13 | route `/front` | Page Manager | `ce_core` route + controller | ✅ implemented, runtime-unverified |
| 14 | `system.site:page.front` | `front` | **`/user/login`** | ⏳ still the installer default |
| 15 | node-id preservation check | — | `ce_migrate.homepage_node_ids` | ✅ ships, never run |
| 16 | templates `node--29--homepage-video` etc. | D7 auto-suggests by nid | `cetc_d10_theme_suggestions_node_alter()` restores | ✅ written, unverified |

**Nothing in this table is newly broken.** Rows 1–6 and 10 are all the same blocker — D10 has no
content. Rows 4, 9 and 12 are the three that are *not* covered by any existing migration and would
not resolve themselves when content arrives.

### The three genuine gaps

1. **Row 4 — `homepage_video` view mode.** No migration in the 45-migration list creates entity
   view modes or view displays. Node 29 would migrate, the controller would ask for the
   `homepage_video` view mode, and Drupal would silently fall back to `default`. The hero would
   render with the wrong formatters and a visible body.
2. **Row 9 — `states_block`.** Code-only in D7, so no data migration reaches it. It must be rebuilt
   as a D10 view, *with* the `field_publish = 1` filter (§6).
3. **Row 12 — the custom pane.** See §0 and H-1.

---

## 9. Open decision H-1 — the testimonials pane

**Not resolved here.** It changes user-visible output and the stated acceptance criterion, so it is
recorded rather than actioned.

> The D7 homepage renders a fifth component: 540 bytes of `full_html` with the heading
> "Your Continuing Education Provider" (`h2`), two customer testimonials, a link to
> `/blog/testimonials`, and a "Get Started" button to `/courses`. It sits **between** the Mailchimp
> block and the education-provider mini-panel. `HomepageController` does not render it.

Three sub-questions, all needing a business/architecture answer:

| | question | note |
|---|---|---|
| H-1a | Where does the HTML live in D10? | a `block_content` entity (module currently **disabled**), a constant in `ce_core`, or a Twig include. A `block_content` block is editable by the client; a constant is not |
| H-1b | Does the acceptance criterion change? | `HOMEPAGE_FLOW.md` §5 says "the same four components, same order". It must say **five** |
| H-1c | Two content details, unchanged from D7 | the testimonials link is **absolute to production** (`https://ceonlinetrainingcourses.com/blog/testimonials`) and will bypass D10; `class="btn btn-secondary"` is Bootstrap-3 markup that `cetc_d10` must carry. Reproducing D7 exactly means keeping both. Changing them is a business decision, not a migration one |

Whichever storage is chosen, the source of truth is the extracted file — its sha256 is recorded in
§0 so the D10 copy can be proven identical to D7's.

---

## 10. What this inventory did NOT verify

Stated so no one mistakes silence for a clean result:

- **Runtime rendering.** Nothing was executed against D10. `/front` has never been requested.
- **Whether `ce_taxonomy_term` carries term-level field data** (`field_publish`). §6 depends on it.
- **Whether `ce_node` preserves nids in practice.** `HomepageNodeIdRequirement` exists to check
  this and has never been run — it cannot pass or fail against 0 nodes.
- **The `newsletter` signup form.** Placed in D7's `cetc_new` content region at weight -19, so it
  appears on the homepage *through the theme*, not through Panels. It is outside this inventory's
  scope but is a real homepage-visible element.
- **Node 36 and node 27.** Template- and webform-bound respectively; tracked in
  `HomepageNodeIdRequirement::TEMPLATE_BOUND_NODE_IDS`, not part of the homepage panes.

---

*Read-only. D7 unchanged: no file, no schema, no row.*
