> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# From DrupalCon keynote to live Upsun demo: the real story of setting up Drupal Canvas with AI

> Discover how the Lemberg Solutions team set up Drupal Canvas with an AI
assistant: how long it took, the challenges we faced and lessons we learned. 


export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud",
      "linkedin": "https://www.linkedin.com/in/laurent-arnoud-861b44121/"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["ribel"], date: "2026-02-13T10:25:00.000Z", image: ">-" }} />

At [DrupalCon Vienna 2025](https://dri.es/state-of-drupal-presentation-october-2025), Dries Buytaert presented a big new feature: Drupal Canvas.
It is a visual page builder in Drupal that allows users to drag and drop to design layouts without touching templates or code.

At [Lemberg Solutions](https://lembergsolutions.com/), we wanted to try it out and show its capabilities to clients as quickly as possible. Instead of setting it up locally, we decided to deploy it live on Upsun (formerly Platform.sh), so anyone could open it in a browser and explore it firsthand.

In this article, we will take you through a step-by-step process, highlight the issues we faced, share the lessons we learned, and reveal how long this task actually took.

# **How our journey looked like: Step-by-step process**

## **Step 1: Write a detailed prompt for AI code generation**

We started in the Cursor IDE with a simple prompt for AI, asking it to act as a senior Drupal and DevOps engineer. We provided all the details that the AI assistant needed to prepare a demo environment. Below, you can see our prompt:

```shell theme={null}
You are a senior Drupal + DevOps engineer helping me prepare a client-facing demo environment for Drupal Canvas on Upsun (formerly Platform.sh).

Drupal Canvas was presented as a major new visual page-building feature in Dries Buytaert’s “State of Drupal” keynote at DrupalCon Vienna 2025.

There is an official demo project here:

        Blog post: https://dri.es/state-of-drupal-presentation-october-2025
        Demo repo: https://github.com/phenaproxima/canvas-demo

I want to adapt this demo to Upsun so that I can spin up short-lived demo environments for clients (preview links), not build a real production site.
```

<img src="https://mintcdn.com/upsun-c9761871/tziXiwEbbKjwbX3l/images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-1.webp?fit=max&auto=format&n=tziXiwEbbKjwbX3l&q=85&s=c6e87202b8279c9a875c4d61a5cf6c9e" alt="AI prompt in Cursor IDE" width="1254" height="640" data-path="images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-1.webp" />

### **Problem #1: The wrong package**

The first issue we faced is that our AI assistant picked up outdated information and used the wrong Composer package name. Specifically, it installed `drupal/experience_builder` instead of `drupal/canvas`.

This happened because during DrupalCon, the module was still referred to as "Experience Builder" in some places, which confused the tool. Using the wrong package can lead to serious compatibility issues later.

**Lesson learned:** Always double-check Composer package names on drupal.org before hitting “Deploy.” AI knowledge isn’t always up to date, so it can rely on irrelevant and old data. A quick manual check can save hours of debugging.

## **Step 2: Check project structure**

After AI created the Drupal structure, ready for Upsun deployment, we spent some time ensuring that the structure of the project had all the needed files. Our final files and folder structure looked like this:

<img src="https://mintcdn.com/upsun-c9761871/tziXiwEbbKjwbX3l/images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-2.webp?fit=max&auto=format&n=tziXiwEbbKjwbX3l&q=85&s=7f56c14014eaa0fbdde39c81d436b224" alt="Final project structure" width="1250" height="646" data-path="images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-2.webp" />

### **Problem #2: Excessive output from AI**

Our AI assistant wanted to be helpful, but went overboard — during the process, it generated 18 extra files. For a quick demo, all we needed was a single README. But it created eight documentation files, multiple setup scripts, and even a “Quickstart Guide” for tools we never planned to use.

**Lesson learned:** When prompting AI, be crystal clear about your intent and what the AI tool should deliver. Don’t say “set up Drupal Canvas on Upsun”, instead, say “set up Drupal Canvas on Platform.sh with DDEV, one README, minimal structure.”

<img src="https://mintcdn.com/upsun-c9761871/tziXiwEbbKjwbX3l/images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-3.webp?fit=max&auto=format&n=tziXiwEbbKjwbX3l&q=85&s=1b26f025dc089174859d6862f125917c" alt="Redundant files generated by AI that were deleted in the end" width="1254" height="926" data-path="images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-3.webp" />

## **Step 3: Check platform configurations**

Our project configuration originally lived in .upsun/config.yaml. It defined a single PHP application with a MariaDB service and pointed web/ as the document root. That’s where the next issue appeared.

### **Problem #3: Platform.sh vs Upsun: name confusion**

Since Platform.sh recently rebranded to Upsun, the AI tool was confused and treated them as two separate platforms. As a result, it created two setups: **Upsun files in .upsun/config.yaml and Platform.sh files in .platform.app.yaml**, along with the entire **.platform/ directory**. To avoid confusion and duplication, we manually deleted one set of files.

**Lesson learned:** Check your git remote and ensure that your project configuration matches the platform you are actually deploying to. Rebrands might confuse AI.

## **Step 4: Build and deploy hooks**

We added simple hooks that allow the site to install itself automatically during deployment. Now, every new environment on Upsun builds a fresh Drupal Canvas instance, with no manual configuration setup required.

```
deploy: |

    set -eux

    # Check if site is already installed

    if ! vendor/bin/drush status bootstrap 2>/dev/null | grep -q 

"Successful"; then

      echo "Installing Drupal site..."

      vendor/bin/drush site:install standard \

        --site-name="Drupal Canvas Demo" \
        --yes

      echo "Enabling Drupal Canvas module..."

      vendor/bin/drush pm:enable canvas --yes

      echo "Site installed successfully!"

    else

    echo "Site already installed, running updates..."

      vendor/bin/drush updatedb --yes
      vendor/bin/drush config:import --yes || true
      vendor/bin/drush cache:rebuild
    fi

    echo "Deployment complete!"
```

### **Problem #4: Wrong Drupal profile**

On the first deployment, we noticed that the AI generated the wrong installation profile name. It tried to install canvas\_demo profile, which doesn’t exist.

**Lesson learned:** After this failure, we switched the installation command to use the standard profile and then explicitly enabled the canvas module inside the deploy hook. That small fix finally made the deployment fully automated and reliable.

<img src="https://mintcdn.com/upsun-c9761871/tziXiwEbbKjwbX3l/images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-4.webp?fit=max&auto=format&n=tziXiwEbbKjwbX3l&q=85&s=f33d6d0021ca5501b659dfb6efac47cf" alt="Platform.sh build log showing “The profile canvas_demo does not exist” error" width="1228" height="654" data-path="images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-4.webp" />

### **The final result**

After all the iterations and fixes, we ended up with a clean, lightweight Drupal Canvas demo, deployed on Upsun and running locally with DDEV. While we didn’t get instant success, now, after several iterations, the demo finally works reliably, and it’s easy to reuse for client demos.

<img src="https://mintcdn.com/upsun-c9761871/tziXiwEbbKjwbX3l/images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-5.webp?fit=max&auto=format&n=tziXiwEbbKjwbX3l&q=85&s=a8ecbcd191fe22e38b81b792fdabcb1b" alt="Final Drupal Canvas page on Upsun" width="1240" height="818" data-path="images/posts/hands-on/the-real-story-of-setting-up-drupal-canvas-with-ai/drupal-canvas-with-ai-5.webp" />

What was the actual timeline for development?
Here is a clear breakdown of the time we spent setting up the Drupal Canvas demo:

| Phase                    | Time           | Key actions                                      |
| :----------------------- | :------------- | :----------------------------------------------- |
| Initial setup            | 3 min          | AI-generated base configs                        |
| First deploy errors      | 10 min         | Config + permission fixes                        |
| Docker troubleshooting   | 15 min         | Build failures, switched tools                   |
| DDEV migration           | 7 min          | Migration completed successfully                 |
| Wrong package fix        | 20 min         | Replaced experience\_builder with canvas         |
| Cleanup & simplification | 30 min         | Deleted docs, folders, cleaned git, final review |
| **Total time**           | **1 h 25 min** | **From start to stable demo**                    |

All in all, AI wrote the initial configurations in about 20 minutes. However, 70% of the work still fell on the human developers, who spent nearly an hour reviewing, fixing, and simplifying the code.

# **Key takeaways**

Working with the AI assistant revealed to us several critical lessons that we want to share.
First, start small and keep it simple. The best way to cooperate with the AI tool is to give well-defined and simple tasks at the beginning. Slowly add complexity within the time. This helps AI to be more accurate and keeps developers from getting stuck on task details.

Second, provide not only context but also concrete examples. If you think you shared enough context at the start, for better results, you may need to write more follow-up prompts with clear examples. This will allow the AI assistant to understand your expectations and deliver results that align with your idea.

Third, human review matters. AI may write code fast, but as you can see above, most of the time it needs human oversight. People still must be in charge of the whole decision-making process.

For more insight into using AI for development, [read our article on vibe coding](https://lembergsolutions.com/blog/vibe-coding-explained-what-buzz-and-can-it-really-cut-development-costs). There we explore which companies can benefit the most from this approach, how to choose the right tools and maintain security.
