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

# Serve static sites

> Serve completely static sites

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>;
};

Static site generators are a popular way to create fast sites.
Because there's no need to wait for responses from servers, the sites may load faster.

To learn how to serve your static site using Upsun,
you can start with the required [minimal app configuration](#minimal-app-configuration) and build on it,
or jump straight to an [example of a complete configuration](#complete-example-configuration).

## Minimal app configuration

To successfully serve a static site using Upsun,
you need to set up a minimal app configuration similar to the following:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          # The type of the application to build.
          type: "nodejs:{{version:nodejs:latest}}"
          source:
            root: "/"
          # The web key configures the web server running in front of your app.
          web:
            locations:
              /:
                # Static site generators usually output built static files to a specific directory.
                # Define this directory (must be an actual directory inside the root directory of your app)
                # as the root for your static site.
                root: "public"
                # Files to consider when serving a request for a directory.
                index:
                  - index.html`
  }
</DynamicCodeBlock>

See more information on the required minimal settings:

* [Top-level properties](/docs/configure-apps/app-reference/single-runtime-image#primary-application-properties).
* [`web` property](/docs/configure-apps/image-properties/web).
* [`locations` properties](/docs/configure-apps/image-properties/web#locations).

## Add more features

### Allow static files but not dynamic files on PHP containers

If you have a PHP container,
you might want to enable client-side scripts but disable server-side scripts.

To enable static files that don't match any rule while disabling server-side scripts on a PHP container,
use the following configuration:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          # The type of the application to build.
          type: "nodejs:{{version:nodejs:latest}}"
          source:
            root: "/"
          web:
            locations:
              '/':
                ...
                scripts: false
                allow: true`
  }
</DynamicCodeBlock>

See more information on [`locations` properties](/docs/configure-apps/image-properties/web#locations).

### Create cache rules

You can create sensible cache rules to improve performance.
For example, if you publish new content regularly without updating images or site files much,
you might want to cache text files for a day but all image files for longer.

To do so, use a configuration similar to the following:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          # The type of the application to build.
          type: "nodejs:{{version:nodejs:latest}}"
          source:
            root: "/"
          web:
            locations:
              '/':
                ...
                expires: 24h
                rules:
                  \\.(css|js|gif|jpe?g|png|svg)$:
                    expires: 4w`
  }
</DynamicCodeBlock>

You can also set a `Cache-Control` header in your rules.

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        <APP_NAME>:
          web:
            locations:
              '/':
                ...
                expires: 24h
                rules:
                  \.(css|js|gif|jpe?g|png|svg)$:
                  headers:
                    Cache-Control: "public, max-age=2419200, immutable"
    `}
</DynamicCodeBlock>

If `expires` and a `Cache-Control` header are set, the rule ignores the `expires` and sets only the `Cache-Control` header. For this reason, make sure
to add a `max-age` value, in seconds, for the `Cache-Control` header.

### Conserve the server

Because your site is completely static, it doesn't need the server to be running.
To set a background process that blocks the server and conserves resources,
use the following configuration:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        <APP_NAME>:
          # The type of the application to build.
          type: "nodejs:{{version:nodejs:latest}}"
          source:
            root: "/"
          web:
            commands:
              start: sleep infinity`
  }
</DynamicCodeBlock>

You can also use this place to start small programs,
such as a [script to handle 404 errors](https://support.platform.sh/hc/en-us/community/posts/16439636723474).

## Complete example configuration

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        <APP_NAME>:
          # The type of the application to build.
          type: "python:{{version:python:latest}}"
          source:
            root: "/"
          web:
            locations:
              '/':
                # The public directory of the application relative to its root
                root: 'public'
                # The files to look for when serving a directory
                index:
                  - 'index.html'
                # Disable server-side scripts
                scripts: false
                allow: true
                # Set caching policy
                expires: 24h
                rules:
                  \\.(css|js|gif|jpe?g|png|svg)$:
                    expires: 4w

            commands:
              # Run a no-op process that uses no CPU resources since this is a static site
              start: sleep infinity`
  }
</DynamicCodeBlock>
