# Deploying to the cPanel host

The target is a shared host: **PHP + MySQL, 0.5 core, 512 MB RAM**. Two rules
follow from that, and they shape every step below:

1. **Never build assets on the server.** The Vite build needs more memory and
   CPU than the host has. `public/build/` is produced on a development machine
   and uploaded.
2. **Never install dependencies on the server if you can avoid it.** Composer
   needs more than 512 MB to resolve a tree. `vendor/` is produced on a
   development machine and uploaded.

Everything else — the migrations, the seeding, `artisan` commands — is light
enough to run on the host.

---

## 1. Prepare on a development machine

```powershell
cd E:\projects\elhg-hassabllah\laravel

# PHP dependencies, without the development packages
composer install --no-dev --optimize-autoloader

# Front-end assets
npm install
npm run build
npm run typecheck
npm run test:js
php artisan test
```

`npm run build` writes `public/build/` (the Vite manifest and the hashed
asset files). `npm run typecheck` must be clean, and both test suites must
pass before anything is uploaded.

## 2. Build the release archive

```powershell
powershell -ExecutionPolicy Bypass -File scripts\release.ps1
```

This produces `release/elhg-hassabllah-<timestamp>.zip` containing exactly
what the server needs:

```
app/       bootstrap/   config/    database/   public/    resources/
routes/    storage/     vendor/    artisan     composer.json
composer.lock   .env.example   DEPLOY.md
```

`vendor/` and `public/build/` are included; `node_modules/`, `.env`, the
local SQLite database, the tests and the source maps are not.

`storage/` travels empty — only the directories the framework writes to
(`framework/cache`, `framework/sessions`, `framework/views`, `logs`). They have
to exist before the first request, so they are part of the archive rather than
something the host has to create.

## 3. Create the database on the host

In cPanel → **MySQL Databases**:

1. Create a database, e.g. `elhg_hassabllah`.
2. Create a user with a strong password.
3. Add the user to the database with **ALL PRIVILEGES**.

Keep the database name, user and password — they go into `.env`.

## 4. Upload

Upload the archive into `public_html/` (or a subdirectory of it), then extract
it. If the host offers **Git Version Control**, pushing the repository and
cloning is faster; the `laravel/` directory of this repository is the app
root.

After extracting, look at the result: `app/`, `bootstrap/`, `config/`,
`public/`, `routes/`, `vendor/` and `artisan` have to be there as **separate
directories and files**. If the archive was unpacked into a handful of files
with odd names — `app\Http\Controllers\StorefrontController.php` as one file —
the archive was written with the wrong directory separators and has to be
repacked before it is uploaded again.

`vendor/` must arrive with its directories intact. A `vendor/` that was
flattened into a single directory means the same thing, and `composer install`
on the host is the fix.

## 5. Point the document root at `public/`

**This is the step that matters most.** The document root must be the `public`
directory, never the application root. Anything outside `public/` — the
`.env` file, `storage/`, `vendor/` — would then be reachable over HTTP.

- cPanel → **Domains** → the domain → **Document Root** → `public_html/abaya/public`
- If the host does not let you choose a subdirectory, put the application in
  `public_html/abaya/` and add a `.htaccess` in `public_html/` that redirects
  into it (see below).

`public/.htaccess` ships with Laravel and handles the front controller.

## 6. Select the PHP version

cPanel → **MultiPHP Manager** → the domain → PHP 8.3.

Laravel 13 needs PHP 8.3 or higher. If the host offers only 8.1 or 8.2,
the application will not boot — ask the host for a newer PHP before going
any further.

Then cPanel → **MultiPHP INI Editor** (or "Select PHP Version" → Options):

| Setting | Value | Why |
|---|---|---|
| `memory_limit` | `512M` | The framework's own limit is the host's ceiling |
| `upload_max_filesize` | `20M` | Product photographs |
| `post_max_size` | `24M` | Must be larger than `upload_max_filesize` |
| `max_execution_time` | `120` | A catalogue write is not instant on this CPU |
| `date.timezone` | `Africa/Cairo` | Matches the store's timezone |
| `opcache.enable` | `1` | The single biggest speed win on shared hosting |

## 7. Configure the environment

Copy `.env.example` to `.env` and fill it in:

```ini
APP_KEY=            # generated in the next step
APP_URL=https://your-domain.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=elhg_hassabllah
DB_USERNAME=elhg_hassabllah
DB_PASSWORD=the-password-from-cpanel

SESSION_DRIVER=database
CACHE_STORE=database

STORE_NAME=الحج حسب الله
STORE_CURRENCY=EGP
STORE_WHATSAPP=201007936360
SITE_URL=https://your-domain.com
```

`SITE_URL` is the public address of the store. It goes into the WhatsApp
message as the link to the product the customer is asking about, so it has to
be the address the customer can open.

`STORE_WHATSAPP` is the store's WhatsApp number — digits only, country code
first, no `+`. Checkout stays disabled while it is empty.

Then generate the application key:

```bash
php artisan key:generate
```

## 8. The certificate

cPanel → **SSL/TLS Status** → **Run AutoSSL**, or install a Let's Encrypt
certificate for the domain. The host usually issues it in a few minutes.

Until the certificate is active the site is served over plain HTTP, and the
sign-in still works: the session cookie is marked secure only when the
request came over HTTPS, so the browser keeps it either way. Once the
certificate is live the cookie becomes secure on its own.

If the host puts a proxy in front of PHP and terminates TLS there, set
`SESSION_SECURE_COOKIE=true` in `.env` instead and clear the config cache —
otherwise the cookie is never marked secure.

## 9. Permissions

```bash
chmod -R 775 storage bootstrap/cache
chmod 644 .env
```

`storage/` and `bootstrap/cache/` are the only directories the application
writes to. On some hosts `storage` is owned by a different user and `chmod` is
refused. In cPanel → **File Manager**, right-click `storage` → **Change
Permissions** → `775`, and tick "Apply to subdirectories". Do the same for
`bootstrap/cache`.

## 10. Create the schema and the first administrator

```bash
php artisan migrate --force
php artisan db:seed --force
```

`db:seed` creates the catalogue (4 categories, 8 products) and the
administrator — but only if `ADMIN_EMAIL` and `ADMIN_PASSWORD` are set in
`.env`. **Delete both lines from `.env` afterwards.**

Use a password of at least twelve characters. A shorter one still works, and
the seeder prints a warning about it, but this account can change the whole
store.

To create or rotate an administrator later:

```bash
php artisan admin:password owner@example.com
php artisan admin:password owner@example.com "a-new-password"
```

## 11. Verify

| Check | Expected |
|---|---|
| `https://your-domain.com/en` | the home page |
| `https://your-domain.com/ar` | the home page in Arabic |
| `https://your-domain.com/ar/collections` | the catalogue |
| `https://your-domain.com/ar/collections?q=عباية` | the catalogue, filtered |
| `https://your-domain.com/ar/products/<a-slug>` | a product page |
| `https://your-domain.com/en/cart` | the cart |
| `https://your-domain.com/admin/login` | the admin login |
| `https://your-domain.com/admin` | the admin sign-in form (not the dashboard) |

Then sign in at `/admin/login` and walk through it:

| Check | Expected |
|---|---|
| the dashboard | the product count and the recent products |
| `/admin/products` | the eight seeded products |
| `/admin/products/<id>` | the editor opens with the product in it |
| `/admin/categories` | the four categories |
| `/admin/settings` | the store settings; change one field and save |
| `/admin/content` | the interface copy editor |
| uploading a photograph | the file appears in `public/uploads/media/` and the thumbnail shows |

Finally, check a URL that does not exist:

| Check | Expected |
|---|---|
| `https://your-domain.com/ar/anything-else` | the branded 404 page, and the status is 404 |

If the styles are missing, `public/build/` was not uploaded. If the page
says "Whoops", `APP_DEBUG` is still `true` — turn it off and clear the
config cache.

---

## Without shell access

Some shared hosts give no SSH. Then:

- **Composer:** run `composer install --no-dev --optimize-autoloader` locally
  and upload `vendor/` (the release script already includes it). If the host
  has cPanel's "Setup Composer" tool, `composer install --no-dev` there also
  works, but it may run out of memory — set a higher `memory_limit` for that
  one command via `php -d memory_limit=1024M artisan` or the INI editor.
- **Artisan:** run the commands through cPanel's **Terminal** if it exists, or
  through a small "run a command" script placed in the document root,
  deleted immediately afterwards:

  ```php
  <?php
  // public/_deploy.php — delete this file straight after running it
  require __DIR__.'/../vendor/autoload.php';
  $app = require_once __DIR__.'/../bootstrap/app.php';
  $kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
  $status = $kernel->handle(
      $input = new Symfony\Component\Console\Input\ArgvInput(['artisan', 'migrate', '--force']),
      new Symfony\Component\Console\Output\ConsoleOutput()
  );
  echo $status;
  ```

  **Never leave this file on the server.** It runs commands without
  authentication.

---

## Updating later

```powershell
git pull                                     # in the app directory
composer install --no-dev --optimize-autoloader
npm install && npm run build                 # locally
# upload the changed files: app/, resources/, public/build/, routes/, config/
php artisan migrate --force
```

`config:cache` and `route:cache` make the app faster on a shared host:

```bash
php artisan config:cache
php artisan route:cache
php artisan event:cache
```

After changing anything in `.env`:

```bash
php artisan config:clear
php artisan config:cache
```

---

## Media

The media files live on the host's own disk:

- The seeded photographs are in `public/media/`.
- The brand mark is in `public/brand/`.
- Uploads from the admin panel are written to `public/uploads/` with a random
  file name, so a re-upload never overwrites an existing file.

`MEDIA_BASE_URL` stays empty. Set it only if the files are served from
somewhere else — an absolute URL that does not start with the media base is
never written to the database, so a mismatched setting silently drops images
rather than corrupting the catalogue.

`public/uploads/` must be writable by PHP:

```bash
chmod -R 775 public/uploads
```

The archive ships `public/uploads/.htaccess`, which denies the request for
any uploaded file that carries a script extension and removes the PHP handler
from that directory. Keep it: without it, an upload would be a way into the
server.
