# Deploying to cPanel

This app is Laravel 13 with a database-backed CMS. On cPanel it needs **PHP 8.3 or newer**, MySQL, and a document root pointed at the `public/` folder. Installation happens in the browser — no terminal, Composer or `.env` editing required.

> **Quick version:** upload the zip, point the domain at `public/`, open `https://yourdomain.com/install.php` and follow the three steps. The wizard checks the server, tests your database, creates the admin account, seeds the site and locks itself.

> If your host only offers PHP 8.2 or older, ask them to enable PHP 8.3/8.4. Laravel 13 will not run on 8.2.

---

## 1. Build the package (on your computer)

```bash
chmod +x scripts/package-cpanel.sh
./scripts/package-cpanel.sh
```

This produces **`dist-cpanel/dge-cpanel.zip`** (about 9 MB), including production-only `vendor/` dependencies, the built Vite assets in `public/build`, the CMS and the deployment files. It excludes your local `.env`, SQLite database, logs, tests, uploaded media and dev packages such as PHPUnit and Faker.

If you prefer to build on the server, upload the repository without `node_modules`, then run `npm ci && npm run build` and `composer install --no-dev --optimize-autoloader` there (requires Node and Composer on the host).

## 2. Prepare cPanel

1. **PHP version** — MultiPHP Manager → select the domain → **PHP 8.3** or **8.4**.
2. **PHP extensions** — MultiPHP INI Editor / Select PHP Version → enable:
   `bcmath`, `ctype`, `curl`, `dom`, `exif`, `fileinfo`, `filter`, `gd`, `hash`, `mbstring`, `openssl`, `pcre`, `pdo_mysql`, `session`, `tokenizer`, `xml`, `zip`.

   `gd` (with WebP support) powers image optimisation: uploads are resized to 2400px and converted to WebP. `exif` lets phone photos keep the right orientation. Without `gd` the site still accepts uploads, but stores the original file untouched. Keep PHP's `memory_limit` at **256M** — encoding a 16-megapixel photo needs roughly 100 MB while it is in memory.
3. **Database** — MySQL Databases:
   - Create a database, e.g. `cpuser_dge`
   - Create a user with a strong password
   - Add the user to the database with **All Privileges**
   - Note the full names (`cpuser_dge`, `cpuser_dgeuser`) for `.env`.
4. **SSL** — run AutoSSL / install the certificate, then enable Force HTTPS.

## 3. Upload the application

1. In **File Manager**, create a folder **outside** `public_html`, for example:
   `/home/cpuser/dge`
2. Upload `dge-cpanel.zip` into it and **Extract**.
3. Confirm the extracted structure looks like:
   ```
   /home/cpuser/dge/artisan
   /home/cpuser/dge/app
   /home/cpuser/dge/public
   /home/cpuser/dge/vendor
   ```
4. There is **no need to create `.env` or run commands** — the browser wizard does both. Continue with step 4.

### Point the domain at `public/` (recommended)

In cPanel → **Domains** → your domain/subdomain → **Manage** → set **Document Root** to:

```
/home/cpuser/dge/public
```

This keeps `.env`, `vendor` and `storage` outside the web root.

### If your host forces `public_html`

Use this only when you cannot change the document root:

1. Copy the **contents** of `dge/public` into `public_html`:
   ```bash
   cp -R /home/cpuser/dge/public/. /home/cpuser/public_html/
   ```
2. Edit `public_html/index.php` so the two require paths point at the app folder:
   ```php
   require __DIR__.'/../dge/vendor/autoload.php';
   $app = require_once __DIR__.'/../dge/bootstrap/app.php';
   ```
3. Keep uploads working by linking the media folder:
   ```bash
   ln -s /home/cpuser/dge/public/uploads /home/cpuser/public_html/uploads
   ```
4. Whenever you rebuild assets, copy `dge/public/build` into `public_html/build` again.

## 4. Run the installation wizard

Open **`https://yourdomain.com/install.php`**. If the domain shows an error before setup, visiting `/` redirects here automatically.

The wizard is plain PHP, so it works before `.env`, `APP_KEY` or the database exist. It runs in three steps:

1. **Server check** — PHP version, required extensions, writable `storage`/`bootstrap/cache`/`public/uploads`, `.env.example` and compiled assets. Red items must be fixed; amber ones (GD, EXIF, etc.) are recommended. Reload the page after changing PHP settings in cPanel.
2. **Database** — site name, site URL and the MySQL database details. Use the full cPanel names, e.g. database `cpuser_dge`, user `cpuser_dgeuser`, host `localhost`. The wizard tests the connection before writing anything. It creates `.env` with a fresh `APP_KEY` and owner-only permissions. **SQLite** is offered as a zero-configuration alternative (good for a test site or a host without MySQL).
3. **Admin account** — name, email and a password of at least 10 characters. On submit the wizard runs migrations, creates the account, seeds the 16 starter content sections and caches config, routes and templates.

When it finishes, use the buttons to open the site and the CMS, then click **Delete install.php** (or remove `public/install.php` in File Manager).

> **If a step fails:** the log under the form shows the command output. Fix the cause and press Install again — migrations, seeds and account creation are all safe to retry.

### What the wizard writes

| Path | Purpose |
| --- | --- |
| `.env` | Database, `APP_KEY`, `APP_URL`, admin credentials (permissions `600`) |
| `storage/app/installed.lock` | Marks the install complete; the wizard refuses to run again |
| `database/database.sqlite` | Only when SQLite is chosen |
| `bootstrap/cache/*.php` | Cached config, routes and compiled views |

> If `.env` already exists (for example from a failed attempt), the wizard keeps it and updates only the fields you submit. An existing `APP_KEY` is never replaced.

### Permissions

The server check tests these folders and reports any that are not writable. If it flags one:

```bash
chmod -R 775 storage bootstrap/cache public/uploads
```

All files should be owned by your cPanel account.

## 5. Sign in and publish

- Site: `https://yourdomain.com`
- CMS: `https://yourdomain.com/admin`
- Sign in with the administrator email and password you set in the wizard.

Use **Brand & settings**, the 16 section editors, **Media library**, **Preview** and **Publish changes**. Every save is live immediately. **Preview** can show the homepage or the portfolio page from your unpublished edits — use the **Home / Work** switch above the frame.

### Taking the site offline while you edit

**Brand & settings → Maintenance mode** has the switch. Publish it and visitors get a branded 503 holding page with your headline, message, email, phone and WhatsApp links; you stay signed in and keep seeing and editing the real site, with an amber banner across the top. `/admin` and `/admin/login` are never blocked, so the switch can always be turned off again. The topbar pill shows **Site live** or **Maintenance mode**, and flags the change as unsaved until you publish.

The holding page is a normal 503 with `Retry-After: 3600`, which tells search engines the closure is temporary. Nothing is deleted while the site is offline: content saves, media uploads and updates all work as usual.

### Change the admin password later

The quickest route is the Terminal:

1. Update `ADMIN_PASSWORD` in `.env`.
2. Run:
   ```bash
   php artisan config:clear
   php artisan db:seed --class=AdminUserSeeder --force
   php artisan config:cache
   ```

Without terminal access, generate a bcrypt hash locally with `php -r "echo password_hash('new-password', PASSWORD_BCRYPT), PHP_EOL;"` and replace the `users.password` value in phpMyAdmin, then clear `bootstrap/cache/config.php` in File Manager.

## 6. Manual installation (optional)

Use this only if the wizard cannot run — for example when the host blocks writes from PHP. The wizard recognises an existing administrator and locks itself automatically.

1. Copy `deploy/.env.cpanel.example` to `/home/cpuser/dge/.env` and fill in `APP_URL`, the `DB_*` values, `ADMIN_EMAIL` and `ADMIN_PASSWORD`.
2. Generate a key:
   ```bash
   cd /home/cpuser/dge
   php artisan key:generate
   ```
3. Create the tables, admin and content:
   ```bash
   php artisan config:clear
   php artisan migrate --seed --force
   php artisan config:cache
   php artisan view:cache
   ```
4. Delete `public/install.php` when you are done.

> Run `--seed` **only on first install**. Re-running `db:seed` overwrites edited section content with the demo content.

## 7. Post-launch checklist

- [ ] `public/install.php` deleted; `https://yourdomain.com/install.php` returns 404
- [ ] `storage/app/installed.lock` exists (wizard completed)
- [ ] `APP_ENV=production` and `APP_DEBUG=false` (the wizard writes these)
- [ ] HTTPS forced; `SESSION_SECURE_COOKIE=true`
- [ ] Admin password saved in a password manager (it was chosen in the wizard)
- [ ] `.env` not readable from the browser (`https://yourdomain.com/.env` must 403/404)
- [ ] Test image upload and confirm the file opens from `/uploads/...`
- [ ] Test the contact links (WhatsApp, email) and update them in the CMS
- [ ] Submit the site in Google Search Console
- [ ] Set up cPanel backups for **files + database**
- [ ] Keep a copy of `dge-cpanel.zip` matching the live version

## Updating the site later

**Content** — edit in the CMS and publish. No deployment needed.

**Code/design** — build once, then apply it in the CMS (recommended) or with File Manager.

### Option A — from the CMS (one click)

1. Build the package locally in the project folder: `./scripts/package-cpanel.sh`. It stamps the build with the version in [`VERSION`](VERSION) and a `deploy-manifest.json`.
2. Sign in to `/admin` and open **Tools → Site updates**.
3. Press **Choose a package (.zip)** and pick `dist-cpanel/dge-cpanel.zip`. The browser sends it in 512 KB parts, so a 2 MB PHP upload limit does not block it.
4. The package is inspected before anything is written: a version, file count and SHA-256 are shown. Press **Apply this update**.
5. When it finishes, press **Reload the CMS**. The site keeps serving during the update.

Every applied package is kept in `storage/app/updates/packages/`, and the build being replaced is snapshotted before the first update writes anything. The **Rollback** card therefore appears straight after an update; press **Roll back to vX.X.X** to put that code and those compiled assets back. Content, settings, uploaded images and the database are not touched. The build you rolled back from is kept too, so a second rollback returns to it (roll forward). Only the current and previous builds are retained.

What the updater does, and does not do:

- Replaces code and compiled assets only. `.env`, the database, `storage/app/installed.lock`, `public/uploads`, logs and caches are never overwritten, and paths outside the app folder are rejected.
- Keeps the applied package and the one it replaced under `storage/app/updates/packages/` (about 9 MB each) so the Site updates screen can roll back without another upload. When no package exists for the installed build, it zips that build first, so even the first update can be undone. A package that is missing or whose checksum no longer matches is refused.
- Deletes `public/install.php` automatically if the package restored it.
- Clears compiled Blade views and `bootstrap/cache` so the next request boots the new code.
- Keeps the previous zip irrelevant to rollback: to undo, upload the older package the same way.
- Stops with a clear message if the server cannot write the files (fix with `chmod -R 775 app public storage`), or if the zip is not a DGE build.

The last applied version is recorded in `storage/app/updates/state.json` and shown on the same screen. Partial uploads older than an hour are cleaned up automatically.

> If PHP runs out of memory while applying a large `vendor/` folder, raise `memory_limit` to 256M in **MultiPHP INI Editor** and retry. The updater streams files, so this is only needed on very restricted hosts.

### Option B — with File Manager

Use this if the CMS itself is unreachable.

1. Build the new zip locally: `./scripts/package-cpanel.sh`.
2. Back up the live `.env`, database and `public/uploads` (cPanel → Backup, plus a zip of `public/uploads`).
3. In **File Manager**, upload the zip into the app folder (`/home/cpuser/dge`) and **Extract**. When asked to overwrite, choose **Yes to all**. Do not delete the folder first — that would take `.env`, your database file (if SQLite), uploads and the install lock with it.
4. Confirm `.env` and `storage/app/installed.lock` are still there. Uploading assets leaves them untouched, but check anyway.
5. Delete `public/install.php` again if the new zip restored it — the wizard is for first installs only and refuses to run once installed.
6. Clear the old caches, either with Terminal:
   ```bash
   cd /home/cpuser/dge
   php artisan config:clear && php artisan view:clear && php artisan config:cache && php artisan view:cache
   ```
   …or **without terminal**: in File Manager delete every file inside `bootstrap/cache/` (keep the `.gitignore` if present). The site rebuilds caches on the next request.
7. Hard-refresh the browser (**Ctrl/Cmd + Shift + R**) on both the site and `/admin` so the new compiled CSS/JS is downloaded instead of the cached copy.
8. Check `/admin` → **Tools → Site updates**: the installed version and last-applied record confirm the new code is live.

> **If your app folder is not writable by the web server**, run `chmod -R 775 storage bootstrap/cache` (and `public/uploads`) once, then retry.

> **Alternative: cPanel Git Version Control.** If your host offers it, create a repository pointing at `/home/cpuser/dge`, commit locally, then **Pull or Deploy**. cPanel refuses to overwrite `.env`, but still check `storage/app/installed.lock` and `public/uploads` afterwards.

## Troubleshooting

| Symptom | Fix |
| --- | --- |
| `500` on every page | Check `storage/logs/laravel.log`; usually `storage` not writable, or a cached config from another host. Delete `bootstrap/cache/*.php`. |
| Wizard says “cannot find the application” | The domain document root must point at `…/dge/public`, or the full app folder must sit next to `public_html`. Uploading only `public/` is not enough. |
| Wizard says “already installed” | An administrator already exists. Open `/admin` instead, and delete `public/install.php`. |
| Install fails on “Creating database tables” | Enable `pdo_mysql`, confirm the user has All Privileges on the database, and check the log shown under the form. |
| `could not find driver` | Enable `pdo_mysql` for the selected PHP version. |
| Assets unstyled after upload | `public/build` was not uploaded, or the document root is wrong. |
| 404 on `/admin` | `mod_rewrite` / `.htaccess` not active, or document root is not `public`. |
| 404 on a page that exists in the code (for example `/portfolio` after a manual upload) | A cached route file from the previous version is still being served. The app now discards a route cache older than its route files, so reload once; if it still 404s, delete every file inside `bootstrap/cache/` (keep `.gitignore`) and confirm `.htaccess` is active and the document root is `public/`. Updates applied from **Tools → Site updates** clear this automatically. If only some links 404 when the site is inside a subfolder, publishing from the new build fixes them: page links are rebuilt from the router. |
| Uploads fail | Make `public/uploads` writable (775); check the PHP upload limit in MultiPHP INI Editor. |
| Login loops | Clear `php artisan config:clear`, confirm `APP_URL` and `SESSION_SECURE_COOKIE` match your HTTPS setup. |
| Changes don't appear | Hard-refresh; content is database-driven. Clear config/view cache if you edited `.env` or Blade files. |
| Draft preview is blank or unstyled | The preview renders the site inside the CMS, so it needs the compiled files in `public/build` and an `APP_URL` that matches the domain used to open `/admin`. Clear the config cache after changing `APP_URL`, then reload. |
| Section colours have no effect | Section colours are inline styles, so they need no rebuild. If only a text colour was set, the band keeps the theme background; use the × in the field to go back to the theme, then publish again. The **Live preview** above the fields shows exactly what will publish, including the contrast ratio — *Custom colours* with no site change means nothing has been published yet. |
| Site updates says “not writable” | Run `chmod -R 775 app public storage` (and `bootstrap/cache`), then reload `/admin`. |
| Rollback is not offered | The snapshot of the previous build could not be written (usually `storage/app` permissions). Fix the permissions and apply the update again, or upload the older `dge-cpanel.zip` to go back. |
| Rollback says the package is missing or damaged | The file in `storage/app/updates/packages/` was removed or changed. Upload that older `dge-cpanel.zip` again instead. |
| “That ZIP is not a DGE build” | You picked the wrong archive. Upload the `dge-cpanel.zip` from `./scripts/package-cpanel.sh`, not a database or media backup. |
| Update upload fails at 100% | Check `storage/logs/laravel.log`; usually `storage/app` is not writable or a security module (`mod_security`) blocks multipart POSTs. Option B still works. |

## Security notes

- Remove `public/install.php` after setup. The wizard locks itself once an administrator exists, but the file should not stay online.
- Never expose `.env`, `vendor`, `storage` or `database` from `public_html`.
- Uploads accept PNG/JPG/WEBP/GIF only, and `public/uploads/.htaccess` blocks script execution.
- The CMS is protected by Laravel sessions, CSRF tokens and login throttling (6 attempts / 15 minutes).
- Content is stored in MySQL; uploaded images live in `public/uploads` and are **not** in the database — include them in backups.
