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

# Set custom headers on static content

> Set custom headers for your static content such as custom content-types or limits to cross-origin usage.

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

When your app responds to dynamic requests, it can generate headers on the fly.
To set headers for static content, add them in [your `web` configuration](/docs/configure-apps/image-properties/web).

You might want to do so to add custom content-type headers, limit what other sites can embed your content,
or allow cross origin requests.

Say you want to limit most files to be embedded only on your site, but you want an exception for Markdown files.
And you want to serve both Markdown and [AAC](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) files with the
correct content types to avoid
[MIME sniffing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing).

Start by defining a header for files in general:

<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:
              "/":
                ...
                # Apply rules to all static files (dynamic files get rules from your app)
                headers:
                  X-Frame-Options: SAMEORIGIN`
  }
</DynamicCodeBlock>

This sets the `X-Frame-Options` header to `SAMEORIGIN` for all static files.
Now your files can only be embedded within your site.

Now set up an exception for Markdown (`*.md`) using a [rule](/docs/configure-apps/image-properties/web#rules):

<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:
              "/":
                #...
                rules:
                  \\.md$:
                    headers:
                    Content-Type: "text/markdown; charset=UTF-8"`
  }
</DynamicCodeBlock>

This rule sets an explicit content type for files that end in `.md`. Because specific rules override the general
heading configuration, Markdown files don't get the `X-Frame-Options` header set before.

<Info>
  <h4>Setting charset</h4>
  By default, [HTTP charset parameters](https://www.w3.org/International/articles/http-charset/index.en) are not sent to the response.
  If not set, modern browsers will detect `ISO-8859-1` and likely default to `windows-1252` as this has 32 more international characters.

  To set the HTTP charset parameters to your desired charset, you can add `; charset=UTF-8` in the `Content-Type` parameters.
</Info>

Now set a rule for AAC files.

<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:
              "/":
                ...
                rules:
                  \\.aac$:
                    headers:
                      X-Frame-Options: SAMEORIGIN
                      Content-Type: audio/aac`
  }
</DynamicCodeBlock>

This rule sets an explicit content type for files that end in `.aac`. It repeats the rule for `X-Frame-Options` because
the `headers` block here overrides the more general configuration.

So now you have three header configurations:

* `X-Frame-Options: SAMEORIGIN` **and** `Content-Type: audio/aac` for AAC files
* Only `Content-Type: text/markdown` for Markdown files
* Only `X-Frame-Options: SAMEORIGIN` for everything else

## Cross origin requests

To allow cross origin requests, add a `Access-Control-Allow-Origin` header to responses.
You can do so for specific origins or for all origins with a wildcard.

<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:
              "/":
                ...
                # Apply rules to all static files (dynamic files get rules from your app)
                headers:
                  Access-Control-Allow-Origin: "*"`
  }
</DynamicCodeBlock>

If you use the wildcard value, the headers are modified for each request in the following ways:

* The value of the `Access-Control-Allow-Origin` header is set to the value of the `Origin` request header.
* The `Vary` header is included with a value of `Origin`. See why in the [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin).

This is done so that credentialed requests can be supported.
They would otherwise fail CORS checks if the wildcard value is used.

## `Strict_Transport_Security` header

The `Strict_Transport_Security` header returns a value of `max-age=0`
unless you enable [HTTP Strict Transport Security (HSTS)](/docs/routes/https#enable-http-strict-transport-security-hsts)
in your [routes configuration](/docs/routes).

Note that once HSTS is enabled, configuration capabilities depend
on the [HSTS properties](/docs/routes/https#enable-http-strict-transport-security-hsts)
set in your routes configuration.
For example, the `max-age` value is set to `31536000` by Upsun and can't be customized.
