# Craft CMS Documentation Craft CMS 5.x documentation covering installation, configuration, templating, and extension points. Last updated: 2026-10-08T11:00:56-07:00 (commit 540fe30ce) --- # Code of Conduct Like the technical community as a whole, the Craft team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people. Diversity is one of our strengths. With that, challenges will arise that can lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance. This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. This code of conduct applies to all spaces used by the Craft community for communication. This includes Craft Discord, Craft CMS Stack Exchange, GitHub, Twitter, Facebook, meetups, conferences, and any other relevant forums. Violations of this code outside of any spaces managed by the Craft team will still affect a person’s ability to participate within them. If you believe someone is violating the code of conduct, we ask that you report it by contacting us from [craftcms.com/contact](https://craftcms.com/contact). We take all reports seriously, and your identity will remain confidential. - **Be friendly and patient.** - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. - **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. - **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Craft community should be respectful when dealing with other members as well as with people outside the Craft community. - **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren’t acceptable. This includes, but is not limited to: - Violent threats or language directed against another person. - Discriminatory jokes and language. - Exclusionary gendered language (guys, gentlemen, bros, etc). - Posting sexually explicit or violent material. - Posting (or threatening to post) other people’s personally identifying information ("doxing"). - Personal insults, especially those using racist or sexist terms. - Unwelcome sexual attention. - Advocating for, or encouraging, any of the above behavior. - Repeated harassment of others. In general, if someone asks you to stop, then stop. - **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Craft is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Craft comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. *Credits for the sources and inspiration of this code of conduct go to the [Django Project](https://www.djangoproject.com/conduct/) and many others.* --- # Configuring Craft Craft can be configured to work in a way that makes sense for you, your team, and your infrastructure. Broadly, configuration refers to how Craft will behave in a particular environment. Most applications will only require [database connection information](#database) to work, but as you get familiar with more of Craft’s features, install plugins, or start using additional services, you may need to provide additional config. We’ll start by looking at how Craft builds its config in a context-aware way, then get into some specific options. Keep in mind that—unlike most aspects of a Craft project—this will require some basic PHP knowledge. [Project config](https://craftcms.com/docs/5.x/system/project-config.html) is a discrete concept, but was designed to integrate with the core config system. We’ll look at some examples in just a moment. ## Where Configuration Happens ### Config Files The most common way to customize your Craft project is by editing files in the [`config/` folder](https://craftcms.com/docs/5.x/system/directory-structure.html). These files act as a canonical map of what customizations you’ve made to a project, and as a bridge between specific settings and [environment variables](#setting-and-resolving-options). | Concern | File(s) | Description | ------- | -------- | ----------- | [General Configuration](#general) | `general.php` | Global options that can affect the front-end, control panel, debugging, etc. | [Database Settings](#database) | `db.php` | Connection settings for your database. | [Custom Options](#custom-settings) | `custom.php` | Arbitrary key-value storage for your own options. | [Routing](#url-rules) | `routes.php` | Custom HTTP routes. | [Redirection](https://craftcms.com/docs/5.x/system/routing.html#redirection) | `redirects.php` | Additional patterns for redirection. | [Application Configuration](#application-configuration) | `app.php`, `app.web.php`, `app.console.php` | Overrides for the root application and any of its [Components](https://www.yiiframework.com/doc/guide/2.0/en/concept-components). | Plugin Settings | `{plugin-handle}.php`, or other custom files | Consult the plugin’s documentation for specifics. | [Advanced](#advanced) | | Specific library options and/or behaviors that may be exposed in a non-standard way, like [Guzzle](#guzzle) or the Twig [sandbox environment](#twig-sandbox). You may find other files in the `config/` folder, like `license.key` or the `project/` folder. Craft (and many plugins) will ask you to place config-adjacent files here, even if they don’t work in a consistent way. Sensitive credentials like your database’s password should be kept out of tracked files—but instead of ignoring config files outright, let’s take a look at some options. ### `.env` New [Craft projects](https://github.com/craftcms/craft) use [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) to load values into the environment from a `.env` file in the root of your project. A basic `.env` file contains pairs of keys and values: ```env CRAFT_APP_ID=my-project CRAFT_ENVIRONMENT=dev # ...and comments! ``` These values can be referenced in your config files by calling [App::env()](craft5:craft\helpers\App::env()), or using them directly in a [control panel setting](#control-panel-settings). Use of PHP’s `getenv()` directly is discouraged, due to [issues with thread-safety](https://github.com/craftcms/cms/issues/3631). The [`getenv()` Twig function](https://craftcms.com/docs/5.x/reference/twig/functions.html#getenv) is a wrapper around our custom `App::env()` method, and therefore fine to use in templates. Craft doesn’t require your variables to follow any kind of naming convention, but it will automatically discover [some specific environment variables](#environment-overrides) for general and database settings. For most installations, the `.env` file is the only place where secrets should be stored. Avoid checking it in to version control! Some platforms (especially those with ephemeral filesystems, like [Craft Cloud](https://craftcms.com/cloud)) provide a GUI for managing environment variables in lieu of using a `.env` file, and will automatically inject them when the server or process starts. `App::env()` is still the recommended method for retrieving environment variables set in this way. Once Craft has fully initialized and the current [site](https://craftcms.com/docs/5.x/system/sites.html) is known, it adds two additional environment variables: Since 5.9.0: $CRAFT_SITE and $CRAFT_SITE_UPPER variables | Variable Name | Value | | --- | --- | | `CRAFT_SITE` | The current site’s handle. | | `CRAFT_SITE_UPPER` | The current site’s handle, converted to `SCREAMING_SNAKE_CASE`. | #### Nested Variables In your `.env` file, one variable can reference another: ```bash BASE_HOSTNAME="acmelabs.com" PRIMARY_SITE_URL="https://${BASE_HOSTNAME}" GLOBAL_SITE_URL="https://global.${BASE_HOSTNAME}" ``` Depending on your infrastructure, this may also be possible at other points in process of loading environment variables. The example above works [thanks to `vlucas/phpdotenv`](https://github.com/vlucas/phpdotenv?tab=readme-ov-file#nesting-variables), the default `.env` file loader; Docker (and therefore DDEV) share this general syntax, but not all variables are available at each step as the container boots up—so interpolation is best left until this last stage. Earlier layers may allow interpolation to be escaped so that it is only evaluated by a later one: ```bash # Here, we escape the beginning interpolation token with a backslash (\): BASE_API_HOSTNAME="api.\${DDEV_HOSTNAME}" ``` Craft does not know or indicate when substitutions have occurred—it only sees the final, fully-resolved values. Variables with substitutions will only be written back into the environment when using one of the [mutable loaders](https://github.com/vlucas/phpdotenv?tab=readme-ov-file#immutability-and-repository-customization). [Control panel settings](#control-panel-settings) can also use interpolation. ### Entry Script Craft will also respond to a handful of [specific environment variables or PHP constants](#bootstrap-config), as long as they are set prior to bootstrapping the application in your entry script. The [starter project](repo:craftcms/craft) shares a `bootstrap.php` file between `web/index.php` and the `craft` executable to consolidate the definition of constants. ### Secrets You can store sensitive values that won’t leak into the process’s environment in a special “secrets” file. The path to this file is determined by the [`CRAFT_SECRETS_PATH` environment variable or constant](#craft_secrets_path). When defined, Craft will attempt to include a file at that path (presumed to be a PHP script that `return`s an associative array), and checks it prior to resolving any environment variable or constant with craft5:craft\helpers\App::env(). ```php return [ 'SUPER_SECRET_KEY' => '...', ]; ``` Even though secrets aren’t automatically loaded into the environment on every request, they can still be accessed via application helpers, Twig templates (like system messages), other PHP scripts, or anyone with direct filesystem access. This is only recommended in situations where environment variable exfiltration is among the last attack surfaces. If your server supports any form of remote access (say, via SSH), this is _not_ a practical security measure! ## Setting and Resolving Options Each setting accepts specific [types and values](#types-and-values) (like an integer, interval expression string, or boolean), but Craft can resolve them in two ways: - **Statically:** A value is set explicitly in a config file, and is the same in all environments. Example: Customizing file types that can be uploaded. - **Dynamically:** Values are only known at runtime, or are set conditionally based on the current environment. Typically, this will involve a call to [App::env()](craft5:craft\helpers\App::env()) using the name of an environment variable that is expected to exist—or whose absence is significant. Example: Dev mode, database connection details, or a storage bucket URL. ### Priority Craft takes the first discovered value, in this order: 0. **Environment Overrides:** For general and database settings, Craft looks for special [environment variables](#environment-overrides) and [PHP constants](#php-constants). 1. **Config Files:** Craft evaluates and merges [multi-environment](#multi-environment-configs) and [application type](#application-types) PHP config files. 2. **Defaults:** Every option has a default value, even if it’s `null`. You can find these defaults in the documentation for each setting. ### Style At its simplest, a config file returns a key-value map of settings: ```php use craft\helpers\App; return [ 'devMode' => App::env('DEV_MODE') ?? false, 'userSessionDuration' => 'P1D', ]; ``` If you are interested in auto-completion and type hints from your editor, the equivalent “fluent” style declaration would look like this: ```php use craft\config\GeneralConfig; use craft\helpers\App; return GeneralConfig::create() ->devMode(App::env('DEV_MODE') ?? false) ->userSessionDuration('P1D'); ``` Each option becomes a method call, accepting the same [values](#types-and-values) that you would provide in a config map. The configuration object is returned to allow chaining. Fluent config is currently available for _general_ and _database_ settings, but unsupported in plugins, [custom](#custom-settings) config files, and [application config](#application-configuration). When in doubt, use a config map! Config files can also return a closure that accepts a single `$config` argument and returns a config object. Since 5.3.0: Closures in config files ```php // config/general.php use craft\config\GeneralConfig; return function(GeneralConfig $config) { return $config ->aliases([ '@webroot' => dirname(__DIR__) . '/web', ]) // ... ; }; ``` Craft will always pass an instance of craft5:craft\config\GeneralConfig or craft5:craft\config\DbConfig to a closure in the primary general or database config files, respectively—but [application type-specific files](#application-types) should accept whatever the _primary_ file returns: ```php general.php return [ 'aliases' => [ '@webroot' => dirname(__DIR__) . '/web', ], ]; ``` ```php general.console.php return function(array $config) { $config['aliases']['@web'] = App::env('CLI_WEB_URL'); return $config; }; ``` There is no equivalent to `GeneralConfig` and `DbConfig` for [application](#application-configuration). If you return an array from the primary file, there is limited value in using a closure in an application type-specific file. The `GeneralConfig` class has a special `addAlias()` method that allows you to merge additional aliases, when using closures _and_ fluent config. ### Application Types Since 5.3.0: Application type-specific config files Craft has two primary application types—_web_ and _console_. Web requests are typically initiated by an HTTP server via `index.php`; console requests are initiated from the `craft` executable, on the command line. You can provide configuration that targets a specific application type by creating additional general, database, or application configuration files with the appropriate suffix:
{{ entry.id }}
{% endif %}
```
```php
// Add `DevMode` header when active:
if (Craft::$app->config->general->devMode) {
Craft::$app->request->headers->set('DevMode', 'On');
}
```
#### Aliases and Environment Variables
In the event you need to evaluate an alias or environment variable directly in Twig or PHP, Craft provides some helpers:
```twig
{# Resolve an alias, recursively: #}
{{ alias('@uploadsBaseUrl') }}
{# Access a specific environment variable: #}
{{ getenv('SOME_ENV_VAR') }}
{# Fully resolve a value, as though it came from a control panel setting: #}
{{ parseEnv(craft.app.config.custom.myDynamicValue) }}
```
```php
// Resolve an alias, recursively:
Craft::getAlias('@uploadsBaseUrl');
// Access a specific environment variable:
craft\helpers\App::env('SOME_ENV_VAR');
// Fully resolve a value, as though it came from a control panel setting:
craft\helpers\App::parseEnv(Craft::$app->config->custom->myDynamicValue);
```
## Config Categories
To make config settings easier to find, they’re organized into a few groups.
### General
[General config settings](https://craftcms.com/docs/5.x/reference/config/general.html) are set via the `config/general.php` file, or using special environment variables.
### Database
Your [database connection settings](https://craftcms.com/docs/5.x/reference/config/db.html) are set via the `config/db.php` file, or using special environment variables.
### URL Rules
You can define custom [URL rules](guide:runtime-routing#url-rules) in `config/routes.php`, and [redirection rules](https://craftcms.com/docs/5.x/system/routing.html#redirection) in `config/redirects.php`. Since 5.6.0: Redirection rules
See [Routing](https://craftcms.com/docs/5.x/system/routing.html) for more details about Craft’s request handling behavior.
### Application Configuration
Some projects may require customization of Craft [application components](https://craftcms.com/docs/5.x/reference/config/app.html) beyond what is exposed through specific config files. This is done directly via the `config/app.php` file.
### Advanced
In rare cases, you may need to customize parts of Craft that don’t follow the normal configuration scheme.
#### Guzzle
Craft uses [Guzzle](http://docs.guzzlephp.org/en/latest/) to make HTTP requests, when…
- …checking for Craft updates;
- …sending a support request from the Craft Support widget;
- …loading RSS feeds from the Feeds widget;
- …working with assets on remote volumes, like Amazon S3;
You can customize the settings passed to Guzzle when initializing these requests by creating a `guzzle.php` file in your `config/` folder. The file does not support Craft’s [multi-environment configuration](#multi-environment-configs) and should only ever return an array with your config overrides at the top level.
```php
return [
'headers' => ['Foo' => 'Bar'],
'query' => ['testing' => '123'],
'auth' => ['username', 'password'],
'proxy' => 'https://myproxy:1234',
];
```
The options defined here will be passed into new `GuzzleHttp\Client` instances. See [Guzzle’s documentation](http://docs.guzzlephp.org/en/latest/) for a list of available options.
To use a proxy for _all_ requests, set an [httpProxy](config5:httpProxy) in general config. This will get merged with the Guzzle configuration, and passed to the front-end for use by JavaScript, in the [control panel](https://craftcms.com/docs/5.x/system/control-panel.html). Setting a proxy only in Guzzle’s config will not affect Ajax requests!
#### Twig Sandbox Since 5.9.0: Twig sandbox rendering
When config5:enableTwigSandbox is _on_, some templates (like [system messages](https://craftcms.com/docs/5.x/system/mail.html#system-messages)) are rendered in a Twig environment governed by a strict security policy.
You can customize this policy via a `twig-sandbox.php` config file, which is merged on top of [Craft’s defaults](repo:craftcms/cms/blob/5.x/src/config/twig-sandbox.php).
```php
[
// Additional Twig tags to allow ...
],
'allowedFilters' => [
// Additional Twig filters to allow ...
],
'allowedFunctions' => [
// Additional Twig functions to allow ...
],
'allowedMethods' => [
// Keys are class names;
// Values are arrays of methods to allow;
craft\elements\Asset::class => [
'getUploader',
],
],
'allowedProperties' => [
// Same format as the above.
],
'allowedClasses' => [
// Each item must be a fully-qualified class name.
// Beware: *all* methods and properties are implicitly allowed!
craft\web\twig\variables\CraftVariable::class,
],
];
```
Internally, Craft uses PHP [attributes](https://www.php.net/manual/en/language.attributes.overview.php) to flag classes, methods, and properties as [allowed](craft5:craft\web\twig\AllowedInSandbox), so they do not appear in the default config array.
If you find that sandboxed rendering is too restrictive, try adding features to the security policy one-by-one, _before_ completely disabling it.
#### HTML Purifier
JSON files containing valid [HTML Purifier configuration](https://htmlpurifier.org/live/configdoc/plain.html) can be added to `config/htmlpurifier/`.
When creating a [Redactor](plugin:redactor) or [CKEditor](plugin:ckeditor) field, you can select one of your predefined purifier configs—or provide a one-off config object. The [`purify`](https://craftcms.com/docs/5.x/reference/twig/filters.html#purify) filter also accepts a reference to an existing config file or a complete config object.
A simple config that scrubs everything but paragraph and anchor tags would look like this:
```json
{
"HTML.AllowedElements": "p, a",
}
```
For security, any keys _not_ set will use their [defaults](https://github.com/ezyang/htmlpurifier/blob/master/plugins/phorum/config.default.php).
Note that HTML Purifier expresses many options with dot notation, like `HTML.AllowedElements`. These are the literal keys, not an indication that keys should be nested!
### Custom Settings
Settings defined in a `config/custom.php` file don’t map to or affect any built-in Craft features, but can useful to centralize data, flags, or secrets that otherwise don’t have a place to live.
```php
return [
'serviceBaseUrl' => 'https://api.service.com/v1',
'servicePublishableKey' => App::env('SERVICE_PUBLISHABLE_KEY'),
'servicePrivateKey' => App::env('SERVICE_PRIVATE_KEY'),
];
```
Custom config follows the same multi-environment structure as other files, so you can use the resolved values in a predictable way in any context:
```twig
{% set publishableKey = craft.app.config.custom.servicePublishableKey %}
{% js "https://cdn.service.com/client.js?key=#{publishableKey}" %}
```
```php
$privateKey = Craft::$app->config->custom->servicePrivateKey;
$client = Craft::createGuzzleClient([
'base_uri' => Craft::$app->config->custom->serviceBaseUrl,
]);
$client->post('/donations', [
'auth' => ['apiuser', $privateKey],
'json' => [
'amount' => 1000,
],
]);
```
If these settings need to be changed frequently, edited by a control panel user, or don’t depend on the environment, they may be a better fit for a [Global Set](https://craftcms.com/docs/5.x/reference/element-types/globals.html).
## Bootstrap Config
Some customization is handled via special variables (set as PHP constants or environment vars) that Craft will take into account as it boots up. Depending on your installation, you may keep these in `web/index.php` and the `craft` CLI entry points, or consolidate common values into a single `required` file, as the [starter project](repo:craftcms/craft) does in its `bootstrap.php` file—they’ll get picked up as long as they’re set prior to calling `$app->run()`.
By virtue of accessing these via craft5:craft\helpers\App::env(), Craft also honors values defined by your environment under the same names or keys. The majority of these settings are tied specifically to the structure of your project directory, though, and generally do not need to change between environments.
Constants you set directly in `web/index.php` will only be defined for _web_ requests, while any you set in the `craft` executable will only be defined for _console_ requests. Use `bootstrap.php` to define constants for _all_ requests.
Bootstrap Variable Reference - View a list of bootstrap variables that Craft uses. - reference/config/bootstrap
---
# Hosting & Deployment
In more cases than not, our recommendations for hosting and deploying a Craft website also describe effective processes for collaborating with teammates. Let’s start by reviewing some general advice on managing a Craft project.
Cloud
## Workflow
Our goal here is to define processes that reduce the likelihood of mistakes making it onto a live website, and to adopt systems that help recover [if/when they do](#common-pitfalls).
### Source Control
No matter how you work—or who you work with—the single most important thing to do when dealing with code is to frequently check it in to [version control](https://www.atlassian.com/git/tutorials/what-is-version-control). Having regular checkpoints and backups can mitigate all kinds of unexpected losses and regressions.
Pushing your work to a centralized repository (GitHub, GitLab, Bitbucket, or elsewhere) also means that getting new code onto a server during a deployment is dramatically simplified.
### System Updates + Project Config
Craft uses Composer under the hood to install and [update](https://craftcms.com/docs/5.x/update.html) plugins and packages it depends on.
Your [`composer.lock`](https://craftcms.com/docs/5.x/system/directory-structure.html#composerlock) file defines the _exact_ set of packages that your project uses, so tracking it in version control is essential. The lockfile is updated any time you run system updates or install a new plugin, making it possible for Composer to reconstruct your entire [`vendor/`](https://craftcms.com/docs/5.x/system/directory-structure.html#vendor) directory.
In a similar manner, changes you make to your site’s settings and content model are tracked by [project config](https://craftcms.com/docs/5.x/system/project-config.html) and written to YAML files in the `config/project/` directory. These files should also be committed to version control.
To stay on top of changes from your team, run these two commands _every time you pull new code_:
1. `composer install`: Ensures the `vendor/` directory contains the expected packages;
1. `php craft up`: Applies incoming project config changes;
We’ll look at how these systems relate to a [typical deployment](#typical-process) in a moment.
### Content
In most cases, your live site’s content should be treated as authoritative: code and configuration flow up (deployment), and content flows back down (database backups).
Craft has a built-in backup tool, accessible from the **Utilities** screen of the control panel (for users with the correct permissions) or as a [console command](https://craftcms.com/docs/5.x/reference/cli.html#db-backup). There’s also a command for [restoring](https://craftcms.com/docs/5.x/reference/cli.html#db-restore) backups!
It is not advisable to attempt merging database tables or otherwise combining local and live content—including completely squashing a live database with a development one. [Project Config](#system-updates-project-config) is Craft’s solution to diverging schema and content.
If you ever need to prepare content ahead of a deployment (say, if you’ve created a new [single](https://craftcms.com/docs/5.x/reference/element-types/entries.html#singles) and can’t risk users seeing a partially blank page), consider using a [content migration](https://craftcms.com/docs/5.x/extend/migrations.html).
## Selecting a Host
Craft will work on virtually any hosting platform that meets its [requirements](https://craftcms.com/docs/5.x/requirements.html), but choosing one that matches your expectations for performance and reliability—and your appetite for devops—is key.
Check out our [introduction to hosting](kb:hosting-craft-101) Knowledge Base article if this is your first time launching a Craft project, or spin up a free seven-day trial of [Craft Cloud](https://craftcms.com/cloud)!
Here are a few things to look out for when reviewing traditional hosting services:
### Recommendations
- Choose a host that offers dedicated resources. This is sometimes referred to as a “virtual private server,” and will often include specific system information when selecting a service plan, like the number of CPU cores, RAM, and disk space allocated to your project.
- For un-managed hosting, consider using a provisioning tool like [Laravel Forge](https://forge.laravel.com/), [ServerPilot](https://serverpilot.io/), or [Ploi](https://ploi.io/) to simplify setup and maintenance.
### Red Flags
- “Shared” hosting may be the most affordable option, but often pits you against other tenants for resources on the same machine.
- Lack of SSH access (i.e. FTP-only hosts) means you will be unable to run [console commands](https://craftcms.com/docs/5.x/reference/cli.html), and significantly limits your [deployment](#deployment) options.
- While convenient, the presence of cPanel is often a sign that the host is re-selling resources, without control over the actual hardware.
### Advanced Options
Craft plays well with non-traditional infrastructure, as well. Let’s look at some examples of how you might take advantage of a provider’s other services.
#### Remote Databases
Your database does not need to be on the same machine that serves web requests. If your provider offers hosted or “managed” databases, consider how your [database settings](https://craftcms.com/docs/5.x/reference/config/db.html) may differ—in most cases, the only difference is what hostname (or [`server`](https://craftcms.com/docs/5.x/reference/config/db.html#server)) you tell Craft to connect to.
Keep in mind that introducing a network roundtrip for each database call can add up quickly. Profile requests with Yii’s debug toolbar to see how this impacts your load times!
A remote database is one prerequisite for [scaling](#scalability) your server horizontally.
#### Scalability
Craft benefits from both vertical and horizontal scaling. Giving your server more compute power and RAM, tuning the HTTP server, and [offloading the database](#remote-databases) and other services (vertical scaling) can bring immediate performance improvements. [Distributing load](kb:configuring-load-balanced-environments) across multiple web servers (horizontal scaling) is also viable, thanks to Craft’s support for centralized [cache](https://craftcms.com/docs/5.x/reference/config/app.html#cache) and [session](https://craftcms.com/docs/5.x/reference/config/app.html#session) storage.
If your site demands global distribution (and can’t be cached at the edge), you may be able to take advantage of read/write splitting, replication, and other [advanced database configuration](https://craftcms.com/docs/5.x/reference/config/app.html#database) options to improve performance across regions.
Choosing the right asset storage medium or [filesystem](https://craftcms.com/docs/5.x/reference/element-types/assets.html#filesystems) is another critical component of hosting—especially when it comes to durability and availability.
#### “Platform as a Service”
Proprietary and open source cloud computing solutions are both options for hosting a Craft application. Services like [Heroku](https://heroku.com/) and the Digital Ocean [App Platform](https://www.digitalocean.com/products/app-platform) are designed to reduce your devops burden by provisioning and scaling resources, as well as integrating build pipelines, workers, release hooks, logs, or alerts.
## Deployment
Broadly, we’re defining _deployment_ as the process of publishing code changes to a live website. For the following examples, we’ll assume your project uses the standard [directory structure](https://craftcms.com/docs/5.x/system/directory-structure.html).
Be sure and read our [Deployment Best Practices](kb:deployment-best-practices) article for some high-level recommendations. What follows is intended for technical users who are tasked with extending their workflow to a web server.
### Typical Process
Let’s look at how to apply a Craft-centric [workflow](#workflow) to deployments. To provide concrete examples, we’ll split the process into three phases:
1. **[Build](#build):** Compile all the code that makes the project run.
1. **[Release](#release):** Expose your new build to traffic.
1. **[Migrate](#migrate):** Bring the database into agreement with the new code.
Once these definitions are clear, check out some [examples](#examples).
These phases aren’t _necessarily_ discrete or sequential, but [isolating each step](#atomic-deployment) can make recovery from a failed deployment much quicker and easier.
#### One-time Setup
Before you deploy a project for the first time, there is apt to be some setup. This might mean uploading an initial batch of assets, creating some config files, setting up SSH keys, importing a database backup, or connecting a service to GitHub. These requirements will emerge as you decide on the specifics of your deploy process—and where each phase of it happens.
#### Build
The build phase can vary widely between sites, tools, and platforms. Our primary goal in this phase is to create a fresh copy of your site’s code in as near-complete a state as possible. This will often involve:
- Cloning the latest code from your repository;
- Installing Composer dependencies;
- Rendering other artifacts like CSS and JavaScript;
Compiling your application may happen any number of places: locally; directly on the live server; or on a separate cloud-based action runner like [Buddy](https://buddy.works) or [GitHub Actions](https://github.com/features/actions).
#### Release
This phase is only concerned with replacing the running website with the new build. In some cases, the _build_ and _release_ phases may partially or completely overlap; in others, a release could involve rerouting traffic to an entirely new container!
This is also a great time to [clear caches](https://craftcms.com/docs/5.x/reference/cli.html#clear-caches)—cached data and templates may be incompatible with the new build.
#### Migrate
The final step in any successful deployment should be running migrations. Craft can apply database migrations and project config changes in a single command:
```bash
php craft up --interactive=0
```
Before any pending migrations are applied, Craft will capture a database backup.
### Examples
With a generic deployment framework in place, we’re ready to get into a few concrete examples.
#### Simple Git
Let’s assume you’ve cloned your project onto a host, and configured it to serve requests directly out of the `web/` directory.
Within the project’s root directory, a simple Git-based deployment might look like this:
```bash
# Fetch new code:
git pull
# Install dependencies:
composer install --no-interaction
# Run migrations and apply project config:
php craft up --interactive=0
# Build front-end artifacts (less urgent / lower priority):
npm install && npm run build
```
Here, the build and release steps are basically combined: updates are integrated directly into the live codebase. For many sites, this is a totally viable process!
If you ever need to roll back after a broken deploy, just check out an earlier commit, run `composer install` (and the `npm` commands, if necessary), and—if Craft had run migrations—restore the automatic database backup from `storage/backups/`.
Take care to not expose your `.git` directory (or other private files) to the web!
#### Atomic Deployment
Let’s look at some ways we might improve upon this simple deployment scheme. Our main goal will be to separate each phase of the deployment, so that the live site spends as little time as possible in an intermediate or partially-updated state.
The combination of `git pull` and `composer install` are the most problematic—but we should also make an effort to pre-build front-end resources. Instead of doing all this work directly in a live directory, let’s move it off to the side.
Suppose your project lives at `/var/www/craft-project/`, and Apache or nginx is configured to use `/var/www/craft-project/web/` as your web root. In `/var/www/`, create two new folders:
```treeview{3,4}
/var/www/
├── craft-project/
├── releases/
└── shared/
```
Within `/var/www/`, a deployment might follow these steps:
```bash
# Get a timestamp to identify this release:
DATE="$(date +'%Y-%m-%d--%H%M%S')" # -> 2023-01-01--123456
# Define paths we’ll be working with:
WORKING="/var/www" # Root deploy directory
RELEASE="$WORKING/releases/$DATE" # New release directory
CURRENT="$WORKING/current" # Live release “link”
# Shallow-clone a fresh copy of the latest project files into our release directory:
git clone --depth=1 git@github.com/organization/repo $RELEASE
# Move into the release directory:
pushd $RELEASE
# Install Composer packages:
composer install --no-interaction
# Build other artifacts:
npm install
npm run build
# Move back out to the main directory:
popd
# Create a symbolic link to the new release:
ln -sf $RELEASE $CURRENT
```
After a deployment, the working directory will look like this:
```treeview{3,5}
/var/www/
├── craft-project/
├── current -> /var/www/releases/2023-01-01--123456/
├── releases/
│ ├── 2023-01-01--123456/
│ └── ...
└── shared/
```
Don’t want to bother with Git, Node, or Composer on your server? The entire “build” step of an atomic deployment can be done elsewhere, then uploaded to the `releases/` directory all at once.
There are a few more pieces to this puzzle, though:
- How do we actually serve our new deployment?
- How we can we persist files between releases?
- What about migrations?
The first is straightforward: your HTTP server’s web root must be updated to`/var/www/current/web/`, taking advantage of the stable [symbolic link](https://en.wikipedia.org/wiki/Symbolic_link) created by the last step in the deployment script. This is a _one-time change_ to the server’s configuration—subsequent deploys will just replace the `current` symlink!
We can also address persistent files with symbolic links. For each file or directory you want to keep between deployments, add a line just before the final `current` link is created. For the sake of clarity, we’ll also define a new variable pointing to the `shared/` directory we created earlier:
```bash
# ...
SHARED="$WORKING/shared"
# Create links to persistent files:
ln -s "$RELEASE/.env" "$SHARED/.env"
ln -s "$RELEASE/web/uploads" "$SHARED/web/uploads"
# Create a symbolic link to the new release:
ln -sf $RELEASE $CURRENT
```
Before this will work, you’ll have to move the “shared” files out of `craft-project/` and into the `shared/` directory. The sub-paths within `shared/` don’t have to be identical, but they should make sense to you and must agree with the script:
```treeview{6,8,13,15}
/var/www/
├── craft-project/
├── current → /var/www/releases/2023-01-01--123456/
├── releases/
│ ├── 2023-01-01--123456/
│ │ ├── .env → /var/www/shared/.env
│ │ ├── web/
│ │ │ ├── uploads/ → /var/www/shared/web/uploads/
│ │ │ └── ...
│ │ └── ...
│ └── ...
└── shared/
├── .env
└── web/
└── uploads/
└── ...
```
Lastly, migrations and project config may still take a moment to apply—it’s up to you to determine how much downtime your project can tolerate! The only thing different from the simple deployment is the `craft` executable path:
```bash
# ...
# Run migrations and apply project config:
php $RELEASE/craft up --interactive=0
```
With an atomic deployment, rollbacks are trivial—the symlink can just be replaced with a previous release, because they’re left exactly as-is.
This is just a blueprint for an “atomic” deployment—there are countless ways to improve upon it for a given site or server. Tools like [Deployer](https://deployer.org/) (a PHP package, itself) provide an abstraction for deployment that is more similar to configuration than scripting, and services like [Laravel Envoyer](https://envoyer.io) roll everything into a neat, hosted solution.
#### Fully-Integrated Platform
On PaaS products like Heroku, you are often only responsible for connecting a repository—the platform figures out what steps are required to build the application and the underlying server “image.” In these cases, the presence of `composer.json` is often enough to indicate that the image needs a PHP runtime and that `composer install` must be run to gather dependencies.
This paradigm also turns the “release” phase on its head: instead of deploying a bundle of _code_, the _entire server image_ (or images, if scaled horizontally) is cycled into the pool of resources and new requests are routed to it—effectively eliminating downtime.
Heroku is just an example of this model, pioneered by Kubernetes and other infrastructure orchestration technologies.
Adopting all or parts of the [twelve-factor app](https://12factor.net/) paradigm can be a little disorienting. In particular, an [ephemeral filesystem](https://craftcms.com/docs/5.x/reference/config/bootstrap.html#craft_ephemeral) means you will need to adjust the way environment variables are set (in lieu of `.env`), how the cache works, and how sessions are stored. Some of these options are discussed in [advanced application configuration](https://craftcms.com/docs/5.x/reference/config/app.html).
## Common Pitfalls
Sometimes, bad things do happen—despite all our preparation. Here are some common issues you might encounter when working with a hosted Craft site.
### Backups
Draft a recovery plan and test it regularly. Backups that exist only on the machine that is being backed up aren’t really backups! Make sure backups that are part of your recovery plan are copied to at least one other off-site location.
### Admin Changes
config5:allowAdminChanges should be disabled in all but development environments. If administrators are allowed to make project config changes directly on a live website without oversight, their changes may be reverted the next time you deploy.
### Dev Mode
config5:devMode is an invaluable debugging tool for local development—but it’s a performance and security liability on a public-facing website. Keep it off by default, and enable it only via you local `.env` file, by adding `CRAFT_DEV_MODE=true`.
Even when `devMode` is off, you can still get profiling data from the Yii debug toolbar by enabling it in your user’s preferences.
### Be Aware of Artifacts
Not everything that comprises a Craft site is (or should be) tracked in version control:
- Assets uploaded to a local [filesystem](https://craftcms.com/docs/5.x/reference/element-types/assets.html#filesystems) via the [control panel](https://craftcms.com/docs/5.x/system/control-panel.html);
- Database backups (stored in [`storage/backups/`](https://craftcms.com/docs/5.x/system/directory-structure.html#storage));
- Secrets (usually [relegated to `.env`](https://craftcms.com/docs/5.x/configure.html#env));
- Caches and other temporary files;
- Compiled CSS or JavaScript;
See the official [starter project](repo:craftcms/craft)’s `.gitignore` file for a more complete list of files we recommend excluding from version control.
When introducing new code onto your server, consider which of the above files should be kept in place and which can be discarded. Fully replacing your web root or project folder is rarely the right move: you shouldn’t need to rebuild configuration or copy files back in after deploying new code, and your client’s uploads should never be deleted or overwritten.
Similarly, if you are using Git directly on a server to pull new code in, how do you reconcile unforeseen changes that happen on-disk?
### Cloud Storage
There are a ton of benefits to using a remote filesystem for your assets, but care must be taken when managing files in a development environment: deleting or moving assets via the control panel can cause problems the live environment to lose track of files in the storage bucket and may lead to broken links.
### Precision + Judgement
Deployments requiring careful timing, quick decision-making, manual intervention, or other non-automated steps should be avoided. Even if the process is well-defined, understood, and practiced, the door remains open to human error.
### The Queue
If you use a [daemonized queue runner](https://craftcms.com/docs/5.x/system/queue.html#daemon) (based on `systemd` or `service`, for instance), it will need to be restarted to pick up the latest application code. [CRON-driven queues](https://craftcms.com/docs/5.x/system/queue.html#cron) load the application at each interval and do not need any special treatment. If the path to your application’s `craft` executable changes on each deployment, consider setting up a reliable symlink, or make the appropriate changes to service configuration files or your `crontab`!
---
# Licensing & Editions
Every Craft project starts with a free **Solo** license, which is valid for public and private use, indefinitely.
Additional features like user management are available by [upgrading](#upgrading) to a paid **Team** or **Pro** license. The current Craft edition and version are displayed at the bottom of every control panel screen.
Throughout the documentation, edition-specific features are called out with Team and Pro badges. If you don’t see a badge on a page or section, it describes features available to all Craft editions.
Features & Pricing - Read more about pricing for Craft editions and services. - https://craftcms.com/pricing
## Upgrading
To try out a paid Craft edition, visit the **Plugin Store** in the [control panel](https://craftcms.com/docs/5.x/system/control-panel.html#plugin-store), and click **Upgrade Craft CMS** in the sidebar:
Selecting an edition will update [project config](https://craftcms.com/docs/5.x/system/project-config.html) and replace the **Try** button with a green **Installed as a trial** message.
## Plugins
Some plugins have editions, as well! The [Plugin Store](https://plugins.craftcms.com/) displays editions in the sidebar, each having a **Try** button. Check the **Editions** tab for more information from the developer.
## Trials
You are allowed to test paid Craft and plugin editions on private domains for as long as you need. However, deploying an unlicensed edition of Craft to a [public domain](kb:how-craft-license-enforcement-works#public-domains) will cause the control panel to display licensing issues to all [administrators](https://craftcms.com/docs/5.x/system/user-management.html#admin-accounts).
Craft CMS & Plugin Trials - Learn how to test and license paid Craft and plugin editions. - https://craftcms.com/knowledge-base/how-to-trial-craft-cms-and-plugin-editions
---
# Installation
The prevalence of modern, mature PHP development tools and infrastructure makes Craft easy to install, run, [upgrade](https://craftcms.com/docs/5.x/upgrade.html), and [deploy](https://craftcms.com/docs/5.x/deploy.html).
This [quick-start](#quick-start) guide focuses solely on setting up a local Craft development environment. If you’re ready to launch, jump to the [hosting](#hosting) or [deployment](#deployment) section.
If at any point you feel stuck, the [Tutorial](https://craftcms.com/docs/5.x/../getting-started-tutorial/index.html) is a comprehensive guide for _anyone_ who wants to get set up with a fast, reliable development environment.
Downloading or installing Craft by any means binds you to its [license](https://craftcms.com/license).
## Quick-Start
Your journey with Craft begins on your local machine, using [DDEV](https://ddev.readthedocs.io/en/stable/). DDEV is a Docker-based PHP development environment that streamlines the creation and management of resources required by a Craft project.
While we [strongly recommend](#why-ddev) DDEV for new Craft projects, [alternate installation methods](#alternative-installation-methods) are available for anyone with a preexisting environment or preferred workflow that meets its [requirements](https://craftcms.com/docs/5.x/requirements.html).
[Install or update DDEV](https://ddev.readthedocs.io/en/stable/users/install/), then follow these steps:
1. Create a project directory and move into it:
```bash
mkdir my-craft-project
cd my-craft-project/
```
1. Create DDEV configuration files:
```bash
ddev config --project-type=craftcms --docroot=web
```
1. Scaffold the project from the official [starter project](https://github.com/craftcms/craft):
```bash
ddev composer create-project "craftcms/craft"
```
The setup wizard will start automatically! Accept all defaults (in `[square brackets]`), and note your chosen username and password.
Our [First-Time Setup](kb:first-time-setup) guide in the Knowledge Base has more information about what to expect during setup.
Congratulations! You now have a fully-functional Craft application installed and configured. Run `ddev launch` to view the starter project’s welcome screen:
## Next Steps
Ready to dive in? Sign in to the [control panel](https://craftcms.com/docs/5.x/system/control-panel.html) by clicking **Go to your control panel** 1 from the welcome screen, or running `ddev launch admin`. The username and password you provided during [setup](kb:first-time-setup) were used to create the first admin user.
You’re welcome to explore things at your own pace—but here are some great starting points:
- Get familiar with the [directory structure](https://craftcms.com/docs/5.x/system/directory-structure.html) that was created during installation;
- Review [configuration](https://craftcms.com/docs/5.x/configure.html) methods and options;
- Explore Craft’s main content tools: [elements](https://craftcms.com/docs/5.x/system/elements.html) and [custom fields](https://craftcms.com/docs/5.x/system/fields.html);
- Run a [console command](https://craftcms.com/docs/5.x/reference/cli.html) using `ddev craft ...` to explore the CLI;
- Discover [plugins](https://craftcms.com/docs/5.x/system/plugins.html) to add features or integrate with other services;
- Find help and inspiration within our vibrant [community](https://craftcms.com/community)!
Done for the day? [`ddev stop`](https://ddev.readthedocs.io/en/stable/users/basics/commands/#stop) will spin down any containers for the project and free up system resources. [`ddev start`](https://ddev.readthedocs.io/en/stable/users/basics/commands/#start) boots everything back up, right where you left off.
### Workflow + Collaboration
We believe that starting with a local development environment (rather than directly on a remote server) fosters a workflow that will support the reliability and longevity of your project.
Defining a Workflow - deploy.md#workflow
To get a collaborator set up, commit your working folder to git (including the `.ddev/` directory) and create a [database backup](https://craftcms.com/docs/5.x/reference/cli.html#db-backup). Have them clone the project, download the backup, and run:
```bash
ddev start
ddev import-db < path/to/backup.sql
```
## Further Reading
### Why DDEV?
DDEV is our recommended development environment because it isolates software required to run Craft from your local machine—and from your other projects. Each of your sites can be spun up with its own database and PHP version, without the need to manage or switch between specific software packages.
The environment for each project is [defined as YAML files](https://ddev.readthedocs.io/en/stable/users/configuration/config/), meaning `ddev start` is usually the only thing required to start working on a project on a new machine—or with a collaborator.
The Docker requirement is not taken lightly! We believe that this one-time installation is much more sustainable for new and returning Craft developers than managing a bare-metal development environment—locally or on a remote server.
### Alternative Installation Methods
Depending on your experience and preferred development environment, you may find one of these platform-agnostic installation processes more comfortable:
- For most environments meeting Craft’s requirements, see [Using the Starter Project](kb:using-the-starter-project) to get started with Composer.
- Especially adventurous users may want to read about [Setting up a Craft Project from Scratch](kb:setting-up-a-craft-project-from-scratch).
### Hosting
The fastest way to share your work is with a free seven-day trial of our first-party hosting platform, [Craft Cloud](https://craftcms.com/cloud).
Selecting a Host - Know your options when looking for a good hosting solution. - deploy.md
Craft’s own footprint is relatively light, but it’s important to choose a platform that matches your traffic, storage, and redundancy needs. We maintain a [list of Craft-friendly hosting products](kb:hosting-craft-101) for projects of varying scale.
### Deployment
deploy.md
There is no one-size-fits-all deployment strategy for a Craft project, but we’ve collected our most salient advice in the [Deployment Best Practices](kb:deployment-best-practices) Knowledge Base article.
Regardless of your target infrastructure, it’s important to define a workflow for yourself and your collaborators. Starting a project from a local environment sets a precedent for the flow of code and configuration; while it is _possible_ to scaffold a project directly on a remote host, maintaining a single source of truth for the site will become difficult with changes being made in multiple places, by multiple parties, or without a means of testing those changes in isolation.
### Troubleshooting
Having trouble with DDEV? Make sure your [Docker](https://ddev.readthedocs.io/en/stable/users/install/docker-installation/#testing-and-troubleshooting-your-docker-installation) installation is working properly, then head to their [troubleshooting](https://ddev.readthedocs.io/en/stable/users/basics/troubleshooting/) page for specific issues.
See the [Troubleshooting a Failed Craft Installation](kb:troubleshooting-failed-installation) Knowledge Base article for more common installation hang-ups.
---
# About Craft CMS
Craft is a flexible, user-friendly CMS for creating custom digital experiences on the web—and beyond.
People who use Craft love its…
- …intuitive, user-friendly [control panel](https://craftcms.com/docs/5.x/system/control-panel.html) for content creation and administrative tasks;
- …clean-slate approach to content modeling and [front-end development](https://craftcms.com/docs/5.x/development/index.html) that doesn’t make any assumptions about your content or how it should be delivered;
- …official Plugin Store with hundreds of free and commercial [plugins](https://plugins.craftcms.com/);
- …robust framework for [plugin development](https://craftcms.com/docs/5.x/extend/index.html);
- …active, vibrant [community](https://craftcms.com/community);
You can learn all about it at [craftcms.com](https://craftcms.com).
## Getting Started
Ready to try out the latest version of Craft? Spin up a [new project](https://craftcms.com/docs/5.x/install.html) or [upgrade](https://craftcms.com/docs/5.x/upgrade.html) an existing one.
The documentation has been rearranged a bit for this release, in order to better highlight the breadth of Craft’s features and consolidate technical material:
- High-level concepts are more visible in the [**System**](https://craftcms.com/docs/5.x/system/index.html) section;
- [**Reference**](https://craftcms.com/docs/5.x/reference/index.html) material has been given a dedicated space, including [element types](https://craftcms.com/docs/5.x/system/elements.html), [field types](https://craftcms.com/docs/5.x/system/fields.html), [console commands](https://craftcms.com/docs/5.x/reference/cli.html), controllers, Twig features, and [configuration](https://craftcms.com/docs/5.x/configure.html).
- Some pages geared toward front-end development have been moved into the [**Development**](https://craftcms.com/docs/5.x/development/index.html) section, including lots of new information on [eager-loading](https://craftcms.com/docs/5.x/development/eager-loading.html) and [collections](https://craftcms.com/docs/5.x/development/collections.html);
- Information about using [forms](https://craftcms.com/docs/5.x/development/forms.html) has been split into a dedicated page, while specific [controller actions](https://craftcms.com/docs/5.x/reference/controller-actions.html) remain in the **Reference** section.
## Tech Specs
Craft is a self-hosted PHP application, built on [Yii 2](https://www.yiiframework.com/). It can connect to MySQL and PostgreSQL [databases](https://craftcms.com/docs/5.x/reference/config/db.html) for content storage. Server-side rendering is powered by [Twig](https://twig.symfony.com), and headless applications have access to content via a [GraphQL API](https://craftcms.com/docs/5.x/development/graphql.html).
View System Requirements - Craft runs best on the latest PHP and MySQL or Postgres versions, but can be configured to work on most modern hosting platforms. - requirements.md
## Diving In
Here are some great ways to begin your journey with Craft:
```
For more control, you can construct `img` tags yourself.
The `alt` attribute can come from anywhere, or be defined via a series of fallbacks:
```twig
{% set thumb = result.articleThumbnail.one() %}
{{ tag('img', {
class: 'article-thumbnail',
src: thumb.getUrl(),
alt: result.thumbnailDescription ?: thumb.alt,
}) }}
```
This example checks a local `thumbnailDescription` [custom field](https://craftcms.com/docs/5.x/system/fields.html) on a `result` entry, and defaults to the `alt` text attached directly to the asset.
Keep in mind that not _every_ use of an image needs alternative text!
This might be a perfectly acceptable way to handle icons attached to [categories](https://craftcms.com/docs/5.x/reference/element-types/categories.html) or [tags](https://craftcms.com/docs/5.x/reference/element-types/tags.html):
```twig
{# Decorative or “presentational” icon: #}
{{ tag('img', {
class: 'icon icon--decorative',
src: myAsset.getUrl(),
height: myAsset.getHeight(),
width: myAsset.getWidth(),
alt: '',
}) }}
```
The [W3C `alt` Decision Tree](https://www.w3.org/WAI/tutorials/images/decision-tree/) is a great resource for deciding what an appropriate alternative text should be.
An asset’s `title` is often _not_ the correct source for descriptions—especially when a file is [uploaded directly to an assets field](https://craftcms.com/docs/5.x/reference/field-types/assets.html):
``` File
# Screenshot:
screenshot_2025-12-31_11-59-59.png
# Photo from digital camera:
IMG_4567.jpg
```
``` Title
# Screenshot:
Screenshot 2025 12 31 11 59 59
# Photo from digital camera:
IMG 4567
```
For this reason, Craft _does not_ attempt to repair a missing `alt` attribute by falling back to the title in the absence of explicit alternative text. Generic or irrelevant alt text is also a barrier for screen reader users, and one that can be difficult to identify without a manual accessibility audit. Instead, we leave the `alt` attribute off entirely so the image can be flagged by automated accessibility tests.
Below, we cover [a few patterns](#alt-text-overrides) that make alternative text easier to maintain.
#### Rich Text
Inserting an image into a rich text editor may not automatically populate `alt` attributes.
- **[Redactor](plugin:redactor)** — Select an inserted image and pick **Edit** to open a modal. Redactor incorrectly labels the alternative text field as its **Title**.
- **[CKEditor](plugin:ckeditor)** — Select an inserted image and pick the **Change image text alternative** button to add a description.
Both plugins also support captions, which convert the image into `