Skip to content

Powered by Grav + Helios

Manual Migration to Grav 2.0

Manual Migration to Grav 2.0

For most sites, the Migrate to Grav 2.0 plugin is the right tool. It automates every step of the process and runs in a clean PHP environment outside of Grav 1.x.

There are setups where the wizard's assumptions do not fit:

  • Multisite installs with shared user folders or per-site environments
  • Complex symlink layouts (for example, user/pages, user/data, or individual plugin folders symlinked into a deployment workflow)
  • Externally-mounted user/ folders (NFS, sshfs, Docker volume mounts)
  • Custom or private plugins that aren't on GPM and don't yet declare 2.0 compatibility
  • Build pipelines where the site is generated by CI rather than edited in place

This page documents the same migration steps the plugin performs, expressed as a procedure you can run yourself. It assumes you have shell access and are comfortable with the standard Grav layout.

Important

The principle of the manual migration is the same as the plugin's: your live 1.x site stays untouched until the very last step. Everything is built up in a staged copy first.

Prerequisites

  • Shell access to the server
  • PHP 8.3 or newer available somewhere on the path (it does not have to be the default PHP for your live site yet)
  • Write access to a directory next to your live webroot for the staged install

Tip

If you can't change the active PHP version on the live host yet, you can do the entire staging and verification on a separate machine that has PHP 8.3+, then rsync the verified stage back into place at promote time.

Step 1: Back Up the Live Site

Take a full backup of the live 1.x install before you change anything:

BASH
cd /path/to/grav-site
bin/grav backup

Or zip user/, backup/, and logs/ manually.

Keep this backup somewhere outside the webroot.

Step 2: Stage a Fresh Grav 2.0 Install

Download the latest stable Grav 2.0 build:

BASH
cd /path/to/grav-site
curl -L -o /tmp/grav-2.0.zip https://getgrav.org/download/core/grav-update/latest

Extract it next to your live install. The archive contains a top-level grav-admin/ folder, which we rename to grav-2/:

BASH
unzip /tmp/grav-2.0.zip -d /tmp/grav-2.0-extract
mv /tmp/grav-2.0-extract/grav-admin ./grav-2

At this point you have a pristine Grav 2.0 install at ./grav-2/ alongside your unchanged Grav 1.x at ./.

Note

If your webroot is served by Apache, the migrate-grav wizard adds an .htaccess rule to prevent the live site's rewrite rules from interfering with /grav-2/. If you hit redirect loops or asset 404s when previewing the stage, add a RewriteRule ^grav-2/ - [L] line near the top of your live .htaccess.

Step 3: Resolve Plugin and Theme Compatibility

This is the step that benefits the most from manual review. For each plugin and theme installed on your live site, you need to decide: install the 2.0 version, leave it disabled, or drop it entirely.

For each package in user/plugins/ and user/themes/:

Check the Compatibility Flag

Open the package's blueprints.yaml and look for a compatibility: block:

YAML
compatibility:
  grav:
    - '1.7'
    - '2.0'
  • Lists 2.0: the package is 2.0-ready. Install via GPM into the stage.
  • Lists 1.7 only: the package is not 2.0-ready. Either skip it, install it disabled, or port it yourself (see below).
  • No compatibility: block: fall back to inference: if dependencies: declares grav >= 2.0, treat as 2.0-compatible; otherwise treat as 1.7-only.

See Plugin Compatibility for the full rules.

Install Compatible Packages Fresh

Do not copy plugin folders across from the 1.x install. Pull each compatible package fresh from GPM into the stage:

BASH
cd grav-2
bin/gpm install <plugin-slug>

For themes:

BASH
bin/gpm install -t <theme-slug>

This guarantees you get the 2.0-ready release rather than the 1.x copy. Many plugins ship significant changes (Composer dependencies, vendor folders, API integration) in their 2.0 versions that copying would miss.

Handle Custom / Private Plugins

For plugins that aren't on GPM:

  1. Copy the plugin folder from user/plugins/<name>/ to grav-2/user/plugins/<name>/.
  2. Open its blueprints.yaml and add a compatibility: block declaring 2.0 once you've verified it works.
  3. If the plugin uses Composer dependencies, run composer install --no-dev inside its folder against PHP 8.3+.
  4. If the plugin has not been ported yet, see the AI-Assisted Development page for a fast path through the porting work.

Disable, Don't Delete

If you have plugins you want to keep on the filesystem but not load on 2.0 (for example, while you wait for an author to release a 2.0 version), copy the folder across and add a config entry to disable it:

grav-2/user/config/plugins/<plugin-name>.yaml:

YAML
enabled: false

This matches the behaviour of the wizard's "disable" policy.

Step 4: Copy User Accounts

Copy the account files across:

BASH
cp -a user/accounts/. grav-2/user/accounts/

The account file format itself is unchanged, but the access control permissions inside each file are not. This is the one part of the accounts copy that needs hand editing, and it's easy to miss because the files load without error either way — the user just silently can't get in.

In Grav 1.7 with admin-classic, admin permissions live under the admin.* namespace:

YAML
access:
  admin:
    login: true
    super: true
  site:
    login: true

Grav 2.0's admin2 is a client of the API plugin, and it checks the api.* namespace instead. An account that only grants admin.login will be refused at the admin2 login screen. For each account file, mirror every admin.* key to a matching api.* key with the same value:

YAML
access:
  admin:
    login: true
    super: true
  api:
    login: true
    super: true
  site:
    login: true

A few details worth knowing:

  • Mirror, don't rename. Keep the admin.* entries in place and add api.* alongside them. Leaving both means the account works whether a given plugin still checks the old namespace or the new one.
  • Both YAML shapes occur. Older accounts sometimes use the dotted-flat form (admin.login: true as a single key under access:) rather than the nested block above. The rule is the same: each admin.X gets a sibling api.X.
  • *`site.permissions are untouched.** Only theadmin.namespace moves; front-endsite.` access carries over as-is.

Note

The Migrate to Grav 2.0 plugin does this conversion for you automatically as it copies the accounts across — it walks each file's access: block and adds the mirrored api.* keys. This manual step exists only because you're doing the copy by hand.

Step 5: Copy Content and Configuration

These directories are content-format-compatible between 1.7 and 2.0 and can be copied across as-is:

BASH
cp -a user/pages/.     grav-2/user/pages/
cp -a user/config/.    grav-2/user/config/
cp -a user/data/.      grav-2/user/data/
cp -a user/env/.       grav-2/user/env/
cp -a user/languages/. grav-2/user/languages/

Tip

Use cp -a (or rsync -a) rather than plain cp so timestamps, permissions, and symlinks are preserved. This matters in particular for any user/pages/ symlinks you have set up for multisite or shared content.

Review Deprecated Config Keys

A small number of system.yaml keys have been renamed or removed in 2.0. The migrate-grav wizard surfaces these with suggested replacements; doing it by hand means scanning system.yaml after the copy and consulting the Grav 2.0 release notes for any keys that are flagged.

Step 6: Verify the Staged Site

Open the stage in a browser. Depending on your webserver configuration, this is one of:

  • http://your-site.test/grav-2/ (relative subdirectory)
  • http://stage.your-site.test/ (separate virtualhost pointing at grav-2/)
  • A separate dev machine where you set up the stage entirely

Verify:

  • The homepage and a representative sample of pages render correctly
  • Admin login works with your existing credentials
  • Each enabled plugin loads without errors (check logs/grav.log)
  • The active theme renders correctly (Grav 2.0 ships with Quark 2 as the default theme; if your old theme has no 2.0 version yet, you may want to switch)
  • Form submissions, login flows, and any plugin-specific functionality work as expected
  • Pages that used Twig in their content ({{ ... }} or {% ... %} in the page body) still render as intended. Grav 2.0 gates this off by default, so those pages show raw Twig until you re-enable it; the Tools → Reports → Twig in Content report lists every affected page. See Twig in Content for how to re-enable it and the safer alternatives

Important

Do not skip this step. Once you promote, rolling back means restoring from the backup zip. Catching issues in the stage is much cheaper.

Step 7: Promote the Stage

This is the only step that modifies the live webroot. Do it during a low-traffic window if you can.

Take a Final Backup

BASH
cd /path/to/grav-site
zip -r migration-backup-$(date +%Y%m%d-%H%M%S).zip . -x 'grav-2/*' 'tmp/*' 'cache/*' 'backup/*'
mv migration-backup-*.zip /somewhere/safe/

Swap the Stage In

Move every top-level entry except grav-2/ aside, then promote the contents of grav-2/:

BASH
cd /path/to/grav-site
mkdir _old
shopt -s extglob dotglob
mv !(grav-2|_old|tmp|cache|backup) _old/ 2>/dev/null
mv grav-2/* grav-2/.* . 2>/dev/null
rmdir grav-2

Warning

Adapt the mv !(grav-2|_old|tmp|cache|backup) _old/ line to your shell. The above uses Bash's extended globbing: shopt -s extglob dotglob enables it and ensures dotfiles (.htaccess, .env) are moved. Test the pattern with echo first.

Clear Caches

BASH
bin/grav clear-cache --all

Verify

Reload the live site. If everything works, you're done. The _old/ directory is your immediate rollback path: rename it back into place if anything is broken.

Step 8: Clean Up

After a day or two of stable operation on 2.0, remove the rollback directory:

BASH
rm -rf _old

Keep the migration backup zip you created in Step 7 indefinitely.

When to Reach for the Plugin Instead

The manual procedure is straightforward but it's also a lot of small careful steps. If, partway through, you realise your setup is in fact close enough to a standard install that the wizard would handle it, the right move is to:

  1. rm -rf grav-2/ to discard the partial manual stage
  2. Install the Migrate to Grav 2.0 plugin on the live 1.x site
  3. Let the wizard handle the rest

There's no penalty for switching paths. The live site has been untouched throughout.