> ## 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.

# Use build and deploy hooks

> Add custom scripts at different stages in the build and deploy process.

export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

As your app goes through the [build and deploy process](/docs/core-concepts/build-deploy),
you might want to run custom commands.
These might include compiling the app, setting the configuration for services based on variables, and rebuilding search indexes.
Do these tasks using one of [three hooks](/docs/configure-apps/hooks/hooks-comparison).

The following example goes through each of these hooks for a multi-app project

* Next.js acts as the frontend container, `client`
* Drupal serves data as the backend container, `api`

Configuration for [both applications](/docs/configure-apps/multi-app) resides in a single [`.upsun/config.yaml` configuration file](/docs/configure-apps).
Be sure to notice the `source.root` property for each.

## Build dependencies

The Next.js app uses Yarn for dependencies, which need to be installed.
Installing dependencies requires writing to disk and doesn't need any relationships with other services.
This makes it perfect for a `build` hook.

In this case, the app has two sets of dependencies:

* For the main app
* For a script to test connections between the apps

Create your `build` hook to install them all:

1. Create a `build` hook in your [app configuration](/docs/configure-apps/app-reference/single-runtime-image):

   ```yaml .upsun/config.yaml theme={null}
   applications:
     client:
       source:
         root: client
       hooks:
         build: |
           set -e
   ```

   The hook has two parts so far:

   * The `|` means the lines that follow can contain a series of commands.
     They aren't interpreted as new YAML properties.
   * Adding `set -e` means that the hook fails if *any* of the commands in it fails.
     Without this setting, the hook fails only if its *final* command fails.

     If a `build` hook fails for any reason, the build is aborted and the deploy doesn't happen.
     Note that this only works for `build` hooks.
     If other hooks fail, the deploy still happens.
2. Install your top-level dependencies inside this `build` hook:

   ```yaml .upsun/config.yaml theme={null}
   applications:
     client:
       source:
         root: client
       hooks:
         build: |
           set -e
           yarn --frozen-lockfile
   ```

   This installs all the dependencies for the main app.

## Configure Drush and Drupal

The example uses [Drush](https://www.drush.org/latest/) to handle routine tasks.
For its configuration, Drush needs the URL of the site.
That means the configuration can't be done in the `build` hook.
During the `build` hook, the site isn't yet deployed and so there is no URL to use in the configuration.
(The [`PLATFORM_ROUTES` variable](/docs/development/variables/use-variables#use-provided-variables) isn't available.)

Add the configuration during the `deploy` hook.
This way you can access the URL before the site accepts requests (unlike in the `post_deploy` hook).

The script also prepares your environment to handle requests,
such as by [rebuilding the cache](https://www.drush.org/latest/commands/cache_rebuild/)
and [updating the database](https://www.drush.org/latest/commands/updatedb/).
Because these steps should be done before the site accepts request, they should be in the `deploy` hook.

All of this configuration and preparation can be handled in a bash script.

1. Copy the [preparation script from the Upsun Fixed template](https://github.com/platformsh-templates/nextjs-drupal/blob/master/api/platformsh-scripts/hooks.deploy.sh)
   into a file called `hooks.deploy.sh` in a `api/platformsh-scripts` directory.
   Note that hooks are executed using the dash shell, not the bash shell used by SSH logins.

2. Copy the [Drush configuration script from the template](https://github.com/platformsh-templates/nextjs-drupal/blob/master/api/drush/platformsh_generate_drush_yml.php)
   into a `drush/platformsh_generate_drush_yml.php` file.

3. Set a [mount](/docs/configure-apps/image-properties/mounts).
   Unlike in the `build` hook, in the `deploy` hook the system is generally read-only.
   So create a mount where you can write the Drush configuration:

   ```yaml .upsun/config.yaml theme={null}
   applications:
     api:
       source:
         root: api

       mounts:
         /.drush:
           source: storage
           source_path: 'drush'
   ```

4. Add a `deploy` hook that runs the preparation script:

   ```yaml .upsun/config.yaml theme={null}
   applications:
     api:
       source:
         root: api

       mounts:
         /.drush:
           source: storage
           source_path: 'drush'

       hooks:
         deploy: !include
           type: string
           path: platformsh-scripts/hooks.deploy.sh
   ```

   This `!include` syntax tells the hook to process the script as if it were included in the YAML file directly.
   This helps with longer and more complicated scripts.

## Get data from Drupal to Next.js

This Next.js app generates a static site.
Often, you would generate the site for Next.js in a `build` hook.
In this case, you first need to get data from Drupal to Next.js.

This means you need to wait until Drupal is accepting requests
and there is a relationship between the two apps.
So the `post_deploy` hook is the perfect place to build your Next.js site.

You can also redeploy the site every time content changes in Drupal.
On redeploys, only the `post_deploy` hook runs,
meaning the Drupal build is reused and Next.js is built again.
So you don't have to rebuild Drupal but you still get fresh content.

1. Set a relationship for Next.js with Drupal.
   This allows the Next.js app to make requests and receive data from the Drupal app.

   ```yaml .upsun/config.yaml theme={null}
   applications:
     client:
       source:
         root: client

       relationships:
         api:
           service: 'api'
           endpoint: 'http'
   ```

2. Set [mounts](/docs/configure-apps/image-properties/workers).
   Like the [`deploy` hook](#configure-drush-and-drupal), the `post_deploy` hook has a read-only file system.
   Create mounts for your Next.js files:

   ```yaml .upsun/config.yaml theme={null}
   applications:
     client:
       source:
         root: client

       mounts:
         /.cache:
           source: tmp
           source_path: 'cache'
         /.next:
           source: storage
           source_path: 'next'
         /.pm2:
           source: storage
           source_path: 'pm2'
         deploy:
           source: storage
           service: files
           source_path: deploy
   ```

3. Add a `post_deploy` hook that first tests the connection between the apps:

   ```yaml .upsun/config.yaml theme={null}
   applications:
     client:
       source:
         root: client
       hooks:
         post_deploy: |
           . deploy/platformsh.environment
           cd platformsh-scripts/test && yarn debug
   ```

   Note that you could add `set -e` here, but even if the job fails, the build/deployment itself can still be counted as successful.

4. Then build the Next.js site:

   ```yaml .upsun/config.yaml theme={null}
   applications:
     client:
       source:
         root: client
       hooks:
         post_deploy: |
           . deploy/platformsh.environment
           cd platformsh-scripts/test && yarn debug
           cd $PLATFORM_APP_DIR && yarn build
   ```

   The `$PLATFORM_APP_DIR` variable represents the app root and can always get you back there.

## Final hooks

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        api:
          # The runtime the app uses.
          type: 'php:{{version:php:latest}}'

          # The relationships of the app with services or other apps.
          relationships:
            database:
              service: 'db'
              endpoint: 'mysql'
            redis:
              service: 'cache'
              endpoint: 'redis'

          # The hooks executed at various points in the lifecycle of the app.
          hooks:
            deploy: !include
            type: string
            path: platformsh-scripts/hooks.deploy.sh

          # The 'mounts' describe writable, persistent filesystem mounts in the app.
          mounts:
            /.drush:
              source: storage
              source_path: 'drush'
            /drush-backups:
              source: storage
              source_path: 'drush-backups'
            deploy:
              source: service
              service: files
              source_path: deploy
        client:
          # The type key specifies the language and version for your app.
          type: 'nodejs:{{version:nodejs:latest}}'

          dependencies:
            nodejs:
              yarn: "1.22.17"
              pm2: "5.2.0"

          build:
            flavor: none

          relationships:
            api:
              service: 'api'
              endpoint: 'http'

          # The hooks that are triggered when the package is deployed.
          hooks:
            build: |
              set -e
              yarn --frozen-lockfile # Install dependencies for the main app
              cd platformsh-scripts/test
              yarn --frozen-lockfile # Install dependencies for the testing script
            # Next.js's build is delayed to the post_deploy hook, when Drupal is available for requests.
            post_deploy: |
              . deploy/platformsh.environment
              cd platformsh-scripts/test && yarn debug
              cd $PLATFORM_APP_DIR && yarn build

          mounts:
              /.cache:
                  source: tmp
                  source_path: 'cache'
              /.next:
                  source: storage
                  source_path: 'next'
              /.pm2:
                  source: storage
                  source_path: 'pm2'
              deploy:
                  source: storage
                  service: files
                  source_path: deploy`
  }
</DynamicCodeBlock>
