From 35a3f375b3ac7cf11b83517db7d4c881791c2d02 Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Fri, 4 Sep 2026 14:45:44 +0000 Subject: [PATCH 1/2] docs(tutorials): add a theming lesson for the front page view and teasers --- .../explanation/component-resolution.md | 2 + docs/nuxt/content/how-to/theming.md | 4 +- docs/nuxt/content/tutorials/README.md | 2 + docs/nuxt/content/tutorials/authentication.md | 2 +- .../content/tutorials/deploy-your-site.md | 2 +- .../content/tutorials/first-custom-module.md | 2 +- .../nuxt/content/tutorials/getting-started.md | 5 +- .../tutorials/theme-your-front-page.md | 283 ++++++++++++++++++ 8 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 docs/nuxt/content/tutorials/theme-your-front-page.md diff --git a/docs/nuxt/content/explanation/component-resolution.md b/docs/nuxt/content/explanation/component-resolution.md index c72b6ec97..ded9cdfcb 100644 --- a/docs/nuxt/content/explanation/component-resolution.md +++ b/docs/nuxt/content/explanation/component-resolution.md @@ -110,6 +110,8 @@ introspection exist to show you which component was chosen. ## Where to go next +- [Theme your front page](/tutorials/theme-your-front-page): the rules + above, applied to a real page, start to finish. - [Theme Druxt components](/how-to/theming): the practical side. - [DruxtModule API reference](/api/packages/druxt/components/DruxtModule): the base class behind everything described here. diff --git a/docs/nuxt/content/how-to/theming.md b/docs/nuxt/content/how-to/theming.md index 0150c7e0f..e96ef3ebd 100644 --- a/docs/nuxt/content/how-to/theming.md +++ b/docs/nuxt/content/how-to/theming.md @@ -5,7 +5,9 @@ description: Change Druxt component output with wrapper components, the default --- > **Before you start:** this guide assumes a working Druxt site (see -> [Getting started](/tutorials/getting-started)). It also helps to have read +> [Getting started](/tutorials/getting-started)). If you have never written +> a wrapper, [Theme your front page](/tutorials/theme-your-front-page) +> walks through two of them end to end. It also helps to have read > [Component resolution](/explanation/component-resolution) for why the > suggestion chain works the way it does. diff --git a/docs/nuxt/content/tutorials/README.md b/docs/nuxt/content/tutorials/README.md index 8c4275dd0..d09607ab4 100644 --- a/docs/nuxt/content/tutorials/README.md +++ b/docs/nuxt/content/tutorials/README.md @@ -13,6 +13,8 @@ lesson. - [Getting started with Druxt](/tutorials/getting-started): set up a Nuxt application powered by a Drupal backend. +- [Theme your front page](/tutorials/theme-your-front-page): replace the + front page listing and its teasers with your own components. - [Add a login flow](/tutorials/authentication): turn the quickstart's already-configured OAuth setup into a real, working login. - [Deploy your site](/tutorials/deploy-your-site): generate the diff --git a/docs/nuxt/content/tutorials/authentication.md b/docs/nuxt/content/tutorials/authentication.md index 74da22db4..505bdd1cb 100644 --- a/docs/nuxt/content/tutorials/authentication.md +++ b/docs/nuxt/content/tutorials/authentication.md @@ -1,6 +1,6 @@ --- title: Add a login flow -weight: -8 +weight: -7 description: Turn the quickstart's already-configured OAuth setup into a real, working login. --- diff --git a/docs/nuxt/content/tutorials/deploy-your-site.md b/docs/nuxt/content/tutorials/deploy-your-site.md index a39a90580..909d8820e 100644 --- a/docs/nuxt/content/tutorials/deploy-your-site.md +++ b/docs/nuxt/content/tutorials/deploy-your-site.md @@ -1,6 +1,6 @@ --- title: Deploy your site -weight: -7 +weight: -6 description: Generate the quickstart as static files, prove why the backend still matters, and put a working Druxt site on a live URL. --- diff --git a/docs/nuxt/content/tutorials/first-custom-module.md b/docs/nuxt/content/tutorials/first-custom-module.md index a2c39ab5b..e5bc39b4c 100644 --- a/docs/nuxt/content/tutorials/first-custom-module.md +++ b/docs/nuxt/content/tutorials/first-custom-module.md @@ -1,6 +1,6 @@ --- title: Building a custom Druxt module -weight: -6 +weight: -5 description: Extend DruxtModule to build your own Druxt-powered, themeable component backed by Drupal data. --- diff --git a/docs/nuxt/content/tutorials/getting-started.md b/docs/nuxt/content/tutorials/getting-started.md index 6b924c1be..935b0bad7 100644 --- a/docs/nuxt/content/tutorials/getting-started.md +++ b/docs/nuxt/content/tutorials/getting-started.md @@ -158,7 +158,7 @@ is the only file you'd change to point the frontend at a different Drupal. ## Where to go next -- Keep learning: [Add a login flow](/tutorials/authentication), +- Keep learning: [Theme your front page](/tutorials/theme-your-front-page), the next lesson. - Log a user in: [Add a login flow](/tutorials/authentication): the OAuth setup this command already provisioned, put to use. @@ -166,7 +166,8 @@ is the only file you'd change to point the frontend at a different Drupal. deployment lesson. - Understand the machine you just started: [Architecture](/explanation/architecture). -- Start customizing the look: [Theme Druxt components](/how-to/theming). +- Reference for the theme layer: + [Theme Druxt components](/how-to/theming). - Browse what each package does: [Druxt modules](/modules). - See finished Druxt sites running in production: [demo.druxtjs.org](https://demo.druxtjs.org). diff --git a/docs/nuxt/content/tutorials/theme-your-front-page.md b/docs/nuxt/content/tutorials/theme-your-front-page.md new file mode 100644 index 000000000..12341c937 --- /dev/null +++ b/docs/nuxt/content/tutorials/theme-your-front-page.md @@ -0,0 +1,283 @@ +--- +title: Theme your front page +weight: -8 +description: Take over the front page view and its teasers with two wrapper components, and learn the naming rule the whole Druxt theme layer runs on. +--- + +Your Druxt site's front page is a Drupal View rendering node teasers. +Both are currently drawn by Druxt's plain fallback output. In this tutorial +you replace them with two components of your own, one for the listing and +one for each teaser. No framework code gets touched. + +By the end you will have written those two files and learned the rule that +decides which file Druxt picks up. That rule is the whole of Druxt +theming. + +## Prerequisites + +- A running Druxt site from + [Getting started with Druxt](/tutorials/getting-started), with + `npm run dev` up. +- **At least two published articles promoted to the front page.** Create + them the way Getting started did (**Content → Add content → Article**), + and on each one open **Promotion options** in the sidebar and tick + **Promoted to front page**. Add an image to one of them. + +Nothing else. You will not install anything or change any configuration. + +**Outcome:** http://localhost:3000 shows your articles' images and body +text, one after another, and **no titles**. That is not a bug, and it is a +useful thing to understand before you theme anything: Drupal's article +teaser display lists the image and the trimmed body, and nothing else. The +heading you'd see on a Drupal site comes from Drupal's own node template, +which a decoupled frontend does not have. Putting the title back is your +job now, and Step 3 does it. + +## Step 1: Find out what is rendering the page + +Before theming something, name it. Ask the backend what the front path +resolves to: + +```sh +curl "$BASE_URL/router/translate-path?path=/" +``` + +(`$BASE_URL` is in your project's `.env`. On the quickstart it holds +`http://127.0.0.1:8888`.) + +**Outcome:** the response names a view. Abridged, the parts that matter +are: + +```json +{ + "resolved": "http://127.0.0.1:8888/node", + "isHomePath": true, + "view": { "view_id": "frontpage", "display_id": "page_1" }, + "jsonapi": { "resourceName": "view--view" } +} +``` + +So the front page is Drupal's **`frontpage`** view, display **`page_1`**. +Druxt renders it with `DruxtView`, and the view's row configuration decides +what each result looks like: `frontpage` renders nodes in the **`teaser`** +display mode, so each row is a `DruxtEntity` in `teaser` mode. + +The listing and the teaser are the two things to theme. + +## Step 2: Write the listing component + +Create this file: + +```vue + + + + +``` + +Save it. Registration, imports and configuration are all handled by the +file's location. + +**Outcome:** reload http://localhost:3000 and the listing now has your +"Latest posts" heading above the articles. + +The path did that. Nuxt turns every file under `components/` into a +component named after its path, so +`components/druxt/view/frontpage/Page1.vue` becomes +**`DruxtViewFrontpage`**`Page1`, and that is exactly the name `DruxtView` +looks for when it renders the `frontpage` view's `page_1` display. The +file's location does the wiring. + +The `results` and `pager` slots are the view's own output, handed to you to +place. A view wrapper is also given `header`, `filters`, `sorts`, +`attachments_before` and `attachments_after`; a slot with nothing behind it +renders nothing, so you only place the ones you want. + +## Step 3: Write the teaser component + +Same idea, one level down. Each result is a node in the `teaser` display +mode: + +```vue + + + + +``` + +**Outcome:** each article is now your `
`, with the +image above the title and the trimmed body below it. The titles are back, +read straight off the entity rather than waiting for a field slot. + +`components/druxt/entity/node/article/Teaser.vue` is +`DruxtEntityNodeArticleTeaser`: entity type `node`, bundle `article`, view +mode `teaser`. The **slot names are the field names** from Drupal's teaser +display for that bundle, so `field_image` and `body` are there because the +article teaser display shows them. Change the display in Drupal and the +slots change with it. + +> The image works because Druxt proxies Drupal's files directory through +> your frontend by default. On a static build there is no proxy at runtime, +> so read [Proxy the Drupal backend through Nuxt](/how-to/proxy) before you +> deploy. + +## Step 4: Declare your props + +Look at your teaser in the browser's element inspector: + +```html +
+``` + +Druxt hands a wrapper a set of props. Anything you don't declare falls +through to `$attrs`, and Vue writes `$attrs` onto your root element, so +undeclared props become literal `[object Object]` attributes in the markup. + +Declare them yourself, or take the module's mixin, which covers the whole +set: + +```vue + +``` + +**Outcome:** reload, and the element is `
`. Only +your own class remains. + +`druxt-views` provides the equivalent for view wrappers as +`DruxtViewsViewMixin`, which declares `count`, `display`, `langcode`, +`mode`, `pager`, `results` and `view`. + +## Step 5: Narrow the teaser's JSON:API request + +A wrapper can narrow the JSON:API request its own component makes. Add a +`druxt` block to the teaser: + +```vue + +``` + +**Outcome:** the titles are still there and the image and body have +vanished. You asked Drupal for one field, so that is all the component has, +and the slots for the fields you didn't request render nothing. + +Put back what the template actually uses: + +```js +fields: ['title', 'body', 'field_image'], +``` + +**Outcome:** image, title and body are all back. + +This is worth remembering as a symptom, not just a feature: **an empty +field slot usually means a `fields` list that doesn't include it**. The +same block also takes `include` for relationship data, which the +[example apps](/how-to/example-apps) use to pull media and taxonomy terms +into a card. + +## Step 6: Theme more than one thing at a time + +The name you choose decides how wide the net is. Drop a level off either +file name and it covers more: + +| File | Component | Themes | +| --- | --- | --- | +| `druxt/entity/node/article/Teaser.vue` | `DruxtEntityNodeArticleTeaser` | article teasers | +| `druxt/entity/node/Teaser.vue` | `DruxtEntityNodeTeaser` | every node type's teaser | +| `druxt/entity/Teaser.vue` | `DruxtEntityTeaser` | every entity's teaser | +| `druxt/view/frontpage/Page1.vue` | `DruxtViewFrontpagePage1` | this one display | +| `druxt/view/Frontpage.vue` | `DruxtViewFrontpage` | every display of the view | + +Try it: rename `article/Teaser.vue` up to `node/Teaser.vue` and your +teaser still renders, now for every content type. Rename it back and the +article-specific file wins again, because **the most specific name that +exists is the one Druxt uses**. Use a general wrapper for your project +style, then add a specific one as an exception. The exception takes over +the moment you save it. + +> A **new** wrapper file appears on the next page load. A **renamed** one +> sometimes doesn't, because the dev server's component index is built from +> what it saw at startup. If a rename seems to be ignored, restart +> `npm run dev`. + +## What you've got + +Your two files, and the rule behind them: + +```text +nuxt/components/druxt/ +├── entity/node/article/Teaser.vue → DruxtEntityNodeArticleTeaser +└── view/frontpage/Page1.vue → DruxtViewFrontpagePage1 +``` + +- The path is the component name, and the component name is the wiring. +- Slots are the module's output: results and pager for a view, one per + field for an entity. +- Undeclared props are written into your markup as attributes. A mixin + declares them for you. +- A wrapper's `druxt.query` decides what its component fetches. +- The most specific matching name wins, so you can theme broadly and then + make exceptions. + +Nothing here is specific to the front page. Every Druxt component on your +site (blocks, menus, breadcrumbs, fields, forms) is themed exactly this +way. + +## Where to go next + +- [Theme Druxt components](/how-to/theming): the reference version of this, + including the default-template alternative to wrapper files and the + dev-mode box that scaffolds a wrapper for you. +- [Component resolution](/explanation/component-resolution): the full + candidate list and how the ranking is built. +- [Debug Druxt with the Vue Devtools](/how-to/devtools): read + `component.options` to see every name a component would accept. +- [Explore the example apps](/how-to/example-apps): finished wrapper sets + for Tailwind, DaisyUI and BootstrapVue. +- [Building a custom Druxt module](/tutorials/first-custom-module): the + same system from the other side, as the author of a themeable component. +- [Deploy your site](/tutorials/deploy-your-site): put the themed site on a + live URL. From c1b5f14f987f5bad6766b5dbc93a3eefc582535f Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Sat, 5 Sep 2026 03:01:31 +0000 Subject: [PATCH 2/2] docs(tutorials): read BASE_URL from .env and close the lesson chain --- docs/nuxt/content/tutorials/theme-your-front-page.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/nuxt/content/tutorials/theme-your-front-page.md b/docs/nuxt/content/tutorials/theme-your-front-page.md index 12341c937..44faded6e 100644 --- a/docs/nuxt/content/tutorials/theme-your-front-page.md +++ b/docs/nuxt/content/tutorials/theme-your-front-page.md @@ -39,11 +39,14 @@ Before theming something, name it. Ask the backend what the front path resolves to: ```sh +BASE_URL=$(grep '^BASE_URL=' .env | cut -d= -f2-) curl "$BASE_URL/router/translate-path?path=/" ``` -(`$BASE_URL` is in your project's `.env`. On the quickstart it holds -`http://127.0.0.1:8888`.) +Run both lines from your project root. The first reads the backend URL out +of `.env`, because `curl` does not load that file itself. Read it rather +than typing a port: setup takes the next free one, so yours may not be +8888. **Outcome:** the response names a view. Abridged, the parts that matter are: @@ -268,6 +271,8 @@ way. ## Where to go next +- Keep learning: [Add a login flow](/tutorials/authentication), the next + lesson. - [Theme Druxt components](/how-to/theming): the reference version of this, including the default-template alternative to wrapper files and the dev-mode box that scaffolds a wrapper for you.