Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c515a8074 | ||
|
|
5fdf55de7f | ||
|
|
f7d739fe2d | ||
|
|
858d505e7e | ||
|
|
240b6d392b | ||
|
|
9762822e97 | ||
|
|
54bee2cf55 | ||
|
|
ef4a4605b2 | ||
|
|
3b31acbbac | ||
|
|
3a526783cb | ||
|
|
1769d5dc0b | ||
|
|
f1159867df | ||
|
|
a6269d18fd | ||
|
|
8f42adc799 | ||
|
|
7f63f064c9 | ||
|
|
c9a08313ca | ||
|
|
0b53569821 | ||
|
|
23cc5a887b | ||
|
|
03aa273100 | ||
|
|
da24dc8c67 |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 161 KiB |
@@ -15,10 +15,10 @@ import Support from '~/content/partials/support-link.mdx';
|
||||
Archival email keeps a long-term, searchable copy of your mail **outside** the live mailbox. It's useful when:
|
||||
|
||||
- You need to retain mail beyond your live mailbox's storage cap.
|
||||
- Compliance or policy requires you keep email for a fixed window.
|
||||
- Compliance or policy requires you keep email long-term.
|
||||
- You want a recovery option for mail you accidentally delete from the live mailbox.
|
||||
|
||||
It's powered by our [Bichon](https://anhonesthost.com/bichon/) archival service.
|
||||
It's powered by the open-source [Bichon](https://github.com/rustmailer/bichon) archival service.
|
||||
|
||||
## How it's different from backups
|
||||
|
||||
@@ -33,8 +33,8 @@ You can use both — they cover different problems.
|
||||
## What's included
|
||||
|
||||
- **Per-mailbox archive.** Enable on the mailboxes that need it, not the whole account. Your plan has an **archival slots** quota; the Email page shows current usage (e.g. `Archival: 0 of 0 mailboxes archived (no archival slots in your plan)` if you haven't added the add-on yet).
|
||||
- **14-day quick-restore window.** Accidentally deleted mail is recoverable without staff help.
|
||||
- **Configurable longer retention.** Set retention to match your policy.
|
||||
- **Retained for the life of your account.** We keep your archive for as long as you have an active account with us — there's no 14-day limit or fixed expiry window.
|
||||
- **Self-service restore.** Mail you accidentally delete from the live mailbox stays in the archive, so you can find and recover it yourself without staff help.
|
||||
- **Independent password reset on the archive.** Grant audit access without disturbing the live mailbox.
|
||||
|
||||
## How to enable
|
||||
|
||||
@@ -83,6 +83,7 @@ For the site-switching steps, see [Switching a site's backend](/whp/how-to/switc
|
||||
|
||||
## Related
|
||||
|
||||
- [Clear your site's cache](/whp/how-to/clear-your-cache/) — what to do when a change doesn't show up right away.
|
||||
- [Add-ons overview](/whp/add-ons/overview/)
|
||||
- [Resource upgrades](/whp/add-ons/resource-upgrades/) — if you need more CPU or RAM rather than a faster cache layer.
|
||||
- [Site Monitoring](/whp/add-ons/monitoring/) — pair with Optimized Webserver to catch any cache-related issues early.
|
||||
|
||||
@@ -27,24 +27,25 @@ import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<Steps>
|
||||
|
||||
1. In the sidebar, click **Domains**.
|
||||

|
||||
1. In the sidebar, click **Domains & DNS**. You'll see a searchable list of the domains on your account.
|
||||

|
||||
|
||||
2. Under **Add New Domain** on the left, type your domain (for example, `example.com`). Don't include `http://` or `www.` — just the bare domain.
|
||||
2. Click **Add Domain** in the top right. In the dialog, type your domain (for example, `example.com`) — just the bare domain, with no `http://` or `www.`
|
||||

|
||||
|
||||
3. Click **Add Domain**.
|
||||
3. Click **Add Domain** to confirm. The domain appears in the list with a green **Active** status.
|
||||
|
||||
</Steps>
|
||||
|
||||
WHP creates the standard set of DNS records automatically for the new domain:
|
||||
WHP creates a DNS zone with the standard set of records automatically for the new domain:
|
||||
|
||||
- **A record** for the apex domain → your server's IP
|
||||
- **CNAME** for `www` → the apex
|
||||
- **NS records** for the nameservers
|
||||
- **MX record** → the mail server
|
||||
- **TXT record** for SPF
|
||||
- **TXT records** for SPF and DKIM
|
||||
|
||||
You can review and tweak any of these from the **DNS Management** panel on the right side of the Domains page (select the domain from the dropdown).
|
||||
You can review and tweak any of these — click **Manage DNS** next to the domain to open the records editor. See [Manage DNS records](/whp/how-to/manage-dns-records/) for the full walkthrough.
|
||||
|
||||
## Point your DNS at us
|
||||
|
||||
@@ -54,7 +55,7 @@ There are two paths depending on where the domain is registered:
|
||||
|
||||
**Registered elsewhere.** At your registrar, do one of the following:
|
||||
|
||||
- Set the **nameservers** to the values shown on the Domains page (recommended — gives us full DNS control, easier to support), **or**
|
||||
- Set the **nameservers** to the values shown in **Manage DNS** (the **NS** records) — recommended, as it gives us full DNS control and is easier to support — **or**
|
||||
- Keep your existing nameservers and add an **A record** pointing the domain to the IP shown on the **Dashboard** page under Server Information.
|
||||
|
||||
## Verify it worked
|
||||
@@ -76,6 +77,7 @@ Once DNS resolves, visiting your domain in a browser will reach WHP — though y
|
||||
|
||||
## Related
|
||||
|
||||
- [Manage DNS records](/whp/how-to/manage-dns-records/)
|
||||
- [Create a site](/whp/how-to/create-a-site/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Backups
|
||||
description: Run on-demand and scheduled backups of your sites and databases, and confirm they're succeeding.
|
||||
sidebar:
|
||||
order: 4
|
||||
order: 5
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Clear your site's cache
|
||||
description: Seeing an old version of a page after making a change? Here's how to clear cached content so your updates show up.
|
||||
sidebar:
|
||||
order: 7
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
If you've changed something on your site but still see the old version — an updated page, a new image, a price, a published post — it's almost always **caching**: a saved copy of the page is being shown to make your site fast. Clearing the cache tells the system to build a fresh copy.
|
||||
|
||||
There are two places a saved copy can live: in **your browser**, and on the **server** (if your site uses our [Optimized Webserver](/whp/add-ons/optimized-webserver/) add-on with LiteSpeed Cache). Work through the steps below in order — the first one fixes most cases.
|
||||
|
||||
<Aside type="note">
|
||||
When you're **logged in** to WordPress, we never serve you a cached page — you always see your site exactly as it is right now. So if a change looks missing while you're logged in, it's almost certainly your browser holding an old copy. Start with a hard refresh.
|
||||
</Aside>
|
||||
|
||||
## Start with a hard refresh
|
||||
|
||||
A normal refresh often reloads the page from your browser's own saved copy. A *hard* refresh forces your browser to fetch everything fresh from the server.
|
||||
|
||||
<Steps>
|
||||
|
||||
1. Open the page that looks out of date.
|
||||
|
||||
2. Do a hard refresh:
|
||||
- **Windows / Linux:** press `Ctrl` + `Shift` + `R`
|
||||
- **Mac:** press `Cmd` + `Shift` + `R`
|
||||
|
||||
3. Still seeing the old version? Open the same page in a **private / incognito window** (which ignores your browser cache entirely). If it looks correct there, the issue was just your browser — clear your browser cache and you're done.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Purge the server cache
|
||||
|
||||
If your site is on the **Optimized Webserver** add-on, pages are also cached at the server by LiteSpeed Cache. Most of the time this clears itself automatically — publishing a post, updating a page, or completing an order purges the right pages for you. Occasionally (after a large change, a theme edit, or a bulk update) you may want to clear it by hand.
|
||||
|
||||
For WordPress sites, you do this from the free **LiteSpeed Cache** plugin:
|
||||
|
||||
<Steps>
|
||||
|
||||
1. Sign in to your site's **WordPress admin** (`yourdomain.com/wp-admin`).
|
||||
|
||||
2. In the left menu, go to **LiteSpeed Cache → Toolbox**.
|
||||
|
||||
3. On the **Purge** tab, click **Purge All**. This drops every cached page; the next visitor to each page gets a freshly built copy.
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="tip">
|
||||
There's also a shortcut in the black toolbar at the top of every WordPress admin page: the **LiteSpeed Cache** menu has a **Purge All** option you can use without leaving the page you're on.
|
||||
</Aside>
|
||||
|
||||
<Aside type="caution">
|
||||
The very first visit to each page after a purge runs at normal (uncached) speed while the fresh copy is built — then it's fast again. Don't judge your site's speed on that first load right after purging.
|
||||
</Aside>
|
||||
|
||||
## If that didn't fix it
|
||||
|
||||
- **Your site isn't WordPress**, or you don't have the LiteSpeed Cache plugin — there's nothing for you to purge directly. Contact us and we'll clear the server-side cache for you.
|
||||
- **You purged everything and still see the old version** — give it a moment and try a hard refresh again. If it persists, it may not be a cache issue at all (for example, a change that didn't actually save, or a content/plugin problem). Reach out and we'll take a look.
|
||||
|
||||
## Related
|
||||
|
||||
- [Optimized Webserver (OpenLiteSpeed + LSCache)](/whp/add-ons/optimized-webserver/) — what the server-level cache is and how to enable it.
|
||||
- [Switching a site's backend](/whp/how-to/switching-site-backend/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Create a site
|
||||
description: Spin up a containerized site on a domain you've added to WHP.
|
||||
sidebar:
|
||||
order: 2
|
||||
order: 3
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Create an email account
|
||||
description: Add a mailbox on one of your domains and connect your email client.
|
||||
sidebar:
|
||||
order: 3
|
||||
order: 4
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
@@ -23,20 +23,52 @@ import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<Steps>
|
||||
|
||||
1. In the sidebar, click **Email**.
|
||||

|
||||
1. In the sidebar, click **Email**. The page is organized into tabs — **Email Accounts**, **Forwarders**, and **Email Domains (DNS)** — and opens on **Email Accounts**. The buttons along the top (**Webmail**, **Admin Panel**, **Setup Instructions**) open the mail server's web tools in a new tab.
|
||||

|
||||
|
||||
2. Scroll to **Email Accounts** and use the form to create a new account on one of your domains. You'll be asked for the domain, the local part, a password, and an optional mailbox size cap.
|
||||
2. On the **Email Accounts** tab, click **Create Email Account** to open the new-account form. You'll be asked for the domain, the local part, a password, and an optional mailbox size cap.
|
||||
|
||||
3. Set a **strong password** — at least 12 characters with a mix of upper case, lower case, numbers, and symbols. Email accounts are common attack targets.
|
||||
|
||||
4. Click **Create**. The new account appears in the **Email Accounts** list.
|
||||
4. Click **Create Account**. The new account appears in the **Email Accounts** list.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Auto-configure your mail app
|
||||
|
||||
Most modern mail apps — Outlook, Apple Mail, Thunderbird, and the iOS and Android mail apps — can set themselves up from your domain's DNS. You enter your **full email address** and **password**, and the app finds the right servers, ports, and security settings on its own.
|
||||
|
||||
**If your domain uses our nameservers, this already works** — we add the necessary records automatically when you add the domain, so there's nothing for you to do.
|
||||
|
||||
### If your DNS is hosted elsewhere
|
||||
|
||||
If your domain's DNS lives at another provider (Cloudflare, GoDaddy, Namecheap, and so on), your mail app can't auto-configure until you add a few records there yourself. The Email page builds the exact records for you: open the **Email Domains (DNS)** tab, find **Autodiscovery Records (DNS)**, pick the domain, and copy them in.
|
||||
|
||||

|
||||
|
||||
Add these records to the domain's zone at your DNS provider. The names are **relative to your domain** — most providers fill in the rest automatically, so `autoconfig` becomes `autoconfig.example.com`.
|
||||
|
||||
| Type | Name | Priority | Weight | Port | Value |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| CNAME | `autoconfig` | — | — | — | your mail server |
|
||||
| SRV | `_autodiscover._tcp` | 0 | 0 | 443 | your mail server |
|
||||
| SRV | `_imaps._tcp` | 0 | 1 | 993 | your mail server |
|
||||
| SRV | `_submission._tcp` | 0 | 1 | 587 | your mail server |
|
||||
| SRV | `_pop3s._tcp` | 0 | 1 | 995 | your mail server |
|
||||
|
||||
Use the **mail server hostname shown in the Autodiscovery Records (DNS) section** as the value — it's the same host your **MX** record points at. The `_pop3s` record is only needed if you read mail over POP3 instead of IMAP. Click **Copy records** to grab them all at once in zone-file format.
|
||||
|
||||
<Aside type="tip">
|
||||
If your provider has a proxy toggle (such as Cloudflare's orange cloud), keep these records **DNS only** — proxying them stops mail clients from reading them.
|
||||
</Aside>
|
||||
|
||||
<Aside type="note">
|
||||
These records only help apps *find* the server. You still create the mailbox in WHP first, and your domain's **MX** record must point at our mail server for mail to be delivered.
|
||||
</Aside>
|
||||
|
||||
## Set up your email client
|
||||
|
||||
The exact IMAP, POP3, and SMTP hostnames are listed on the Email page — click **Setup Instructions → View Instructions** under **Mail Server Access** for a step-by-step that includes the right hostnames, ports, and security settings for your server.
|
||||
Most apps configure themselves from the records above once you enter your address and password. If yours doesn't support that — or you'd rather enter the settings by hand — the exact IMAP, POP3, and SMTP hostnames are listed on the Email page: click **Setup Instructions** at the top of the page for a step-by-step that includes the right hostnames, ports, and security settings for your server.
|
||||
|
||||
The typical settings look like this; substitute the hostname shown in the Setup Instructions:
|
||||
|
||||
@@ -50,33 +82,35 @@ IMAP (incoming)
|
||||
|
||||
SMTP (outgoing)
|
||||
Host: <see Setup Instructions>
|
||||
Port: 587
|
||||
Security: STARTTLS
|
||||
Port: 465
|
||||
Security: SSL/TLS
|
||||
Username: full email address
|
||||
Password: same as IMAP
|
||||
```
|
||||
|
||||
For outgoing mail, **port 465 with SSL/TLS** is the standard. If your client prefers STARTTLS, **port 587** is the alternate submission port. (Don't use port 25 from a mail client — it's for server-to-server delivery and most networks block it.)
|
||||
|
||||
For per-client walkthroughs (Outlook, Apple Mail, Thunderbird, etc.), see the Email clients section — coming soon.
|
||||
|
||||
## Webmail
|
||||
|
||||
Click **Webmail Access → Open Webmail** on the Email page to sign in to webmail in a new tab.
|
||||
Click **Webmail** at the top of the Email page to sign in to webmail in a new tab.
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Send yourself a test message from another account (your personal Gmail, for example). It should arrive within a minute or two and be retrievable from both your client and webmail.
|
||||
|
||||
<Aside type="caution">
|
||||
**SPF and DKIM records matter.** Without them, your outgoing mail will get flagged or rejected by other providers. We add an SPF record automatically when you add a domain. DKIM records are listed in the **DKIM Records** section near the bottom of the Email page — make sure they're present at your registrar if the domain isn't using our nameservers.
|
||||
**SPF and DKIM records matter.** Without them, your outgoing mail will get flagged or rejected by other providers. We add an SPF record automatically when you add a domain. DKIM records are listed in the **DKIM Management** section on the **Email Domains (DNS)** tab — make sure they're present at your registrar if the domain isn't using our nameservers.
|
||||
</Aside>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Webmail isn't reachable.** DNS for the mail subdomain may still be propagating — wait an hour and try again.
|
||||
**Webmail isn't reachable.** Webmail is hosted at our address — the **Webmail** button on the Email page opens it directly — so it doesn't depend on your domain or its DNS. If it doesn't load, it's almost always a temporary connection issue: try again in a few minutes or from another network, and open a support ticket if it persists.
|
||||
|
||||
**Outgoing mail is bouncing or going to spam.** Check the SPF and DKIM records. The DKIM Records panel on the Email page shows whether DKIM is configured for each of your domains.
|
||||
**Outgoing mail is bouncing or going to spam.** Check the SPF and DKIM records. The **DKIM Management** section on the **Email Domains (DNS)** tab shows whether DKIM is configured for each of your domains.
|
||||
|
||||
**Client can connect on IMAP but not SMTP.** Some ISPs and corporate networks block outgoing port 587. Try sending from a different network to confirm; if the issue is your network, your ISP is the place to ask.
|
||||
**Client can connect on IMAP but not SMTP.** Some ISPs and corporate networks block outgoing mail ports. If sending fails on port 465, try the alternate submission port **587** (STARTTLS); if both fail, test from a different network to confirm it's your network, and if so, your ISP is the place to ask.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Manage DNS records
|
||||
description: View, add, edit, and delete DNS records for your domains using the Domains & DNS records editor in WHP.
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
import SignIn from '~/content/partials/signing-in.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
When we host your DNS, WHP gives you a full records editor — add, edit, or remove **A**, **CNAME**, **MX**, **TXT**, and other records yourself, no support ticket needed.
|
||||
|
||||
## Before you start
|
||||
|
||||
- A domain already added to your account. If you haven't done that yet, [add a domain](/whp/how-to/add-a-domain/) first.
|
||||
- We must be running DNS for the domain (its nameservers point at us). If your DNS lives at another provider, make these changes there instead.
|
||||
- A couple of minutes. Record changes apply quickly on our side, but can take up to 24 hours to propagate worldwide.
|
||||
|
||||
## Sign in to WHP
|
||||
|
||||
<SignIn />
|
||||
|
||||
## Open the records editor
|
||||
|
||||
<Steps>
|
||||
|
||||
1. In the sidebar, click **Domains & DNS**.
|
||||

|
||||
|
||||
2. Find your domain in the list and click **Manage DNS**.
|
||||

|
||||
|
||||
</Steps>
|
||||
|
||||
Each domain starts with a standard zone created automatically when the domain was added:
|
||||
|
||||
| Type | What it's for |
|
||||
| --- | --- |
|
||||
| **A** | Points the apex domain at your server's IP. |
|
||||
| **CNAME** | Aliases like `www` and `autoconfig` to the right host. |
|
||||
| **MX** | Routes mail for the domain to our mail server. |
|
||||
| **TXT** | SPF and DKIM records that help your mail pass authentication. |
|
||||
| **NS** | The nameservers that are authoritative for the domain. |
|
||||
| **SRV** | Service records such as mail autodiscovery. |
|
||||
|
||||
<Aside type="caution">
|
||||
Editing **MX**, **NS**, or the SPF/DKIM **TXT** records can break email delivery or hand DNS control away from us. Only change these if you know exactly what you're doing.
|
||||
</Aside>
|
||||
|
||||
## Mail autodiscovery records
|
||||
|
||||
When we host your DNS, the records that let mail apps configure themselves — an `autoconfig` CNAME plus a set of `_autodiscover` / `_imaps` / `_submission` / `_pop3s` **SRV** records — are already in your zone. You don't need to add them.
|
||||
|
||||
If your DNS is at another provider, add them there by hand. WHP builds the exact records for each domain on the **Email** page — see [Auto-configure your mail app](/whp/how-to/create-an-email-account/#auto-configure-your-mail-app).
|
||||
|
||||
## Add a record
|
||||
|
||||
<Steps>
|
||||
|
||||
1. Click **Add Record**. A new, editable row appears at the top of the table.
|
||||

|
||||
|
||||
2. Fill in the row:
|
||||
- **Type** — choose the record type (A, AAAA, CNAME, MX, TXT, NS, PTR, SRV, CAA, SSHFP, or TLSA).
|
||||
- **Name** — the host the record applies to. Use `@` for the domain itself, or a subdomain like `blog` for `blog.example.com`.
|
||||
- **Content** — the value: an IP for an A record, a hostname for a CNAME, and so on.
|
||||
- **Prio** — only used by record types that need a priority (like MX). Leave it blank otherwise.
|
||||
- **TTL** — how long resolvers may cache the record, in seconds. The default of `300` (5 minutes) is fine for most records.
|
||||
|
||||
3. Click **Save**. The record joins the list immediately. (Click **Cancel** to discard the row without saving.)
|
||||
|
||||
</Steps>
|
||||
|
||||
## Edit or delete a record
|
||||
|
||||
In the **Actions** column on the right of each row:
|
||||
|
||||
- The **pencil** icon opens the record for editing in place. Change any field, then save.
|
||||
- The **trash** icon deletes the record. Deletions take effect right away, so double-check before you remove anything.
|
||||
|
||||
## Find a record quickly
|
||||
|
||||
If a domain has a lot of records, use the **All types** dropdown above the table to filter by a single record type — for example, show only **MX** records while you sort out mail.
|
||||
|
||||
## Work with several records at once
|
||||
|
||||
Tick the checkboxes on the left of one or more rows to reveal the bulk-action bar:
|
||||
|
||||

|
||||
|
||||
- **Change TTL** — set the same TTL on every selected record.
|
||||
- **Enable/Disable** — toggle records on or off without deleting them (handy for temporarily parking a record).
|
||||
- **Delete** — remove all selected records at once.
|
||||
- **Clear** — clear your selection (this does *not* delete anything).
|
||||
|
||||
## Verify it worked
|
||||
|
||||
DNS changes apply on our side within moments, but resolvers elsewhere may keep serving the old answer until the record's TTL expires (up to 24 hours for unfamiliar records).
|
||||
|
||||
- Run `dig example.com +short` (or `dig blog.example.com A +short`) from a terminal and confirm you see the value you just set.
|
||||
- Or use a web tool like [whatsmydns.net](https://www.whatsmydns.net/) to watch propagation across regions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**My change isn't showing up yet.** Resolvers cache records for the length of their TTL. Wait for the TTL to pass, then clear your local DNS cache and check again with `dig`.
|
||||
|
||||
**There's no Manage DNS button for my domain.** We're not running DNS for it — its nameservers point somewhere else. Make the change at your current DNS provider, or [point the domain's nameservers at us](/whp/how-to/add-a-domain/#point-your-dns-at-us) first.
|
||||
|
||||
**Email stopped working after I edited records.** Restore the original **MX** and SPF/DKIM **TXT** records. If you're not sure what they should be, open a support ticket (see below) and we'll put them back.
|
||||
|
||||
## Related
|
||||
|
||||
- [Add a domain](/whp/how-to/add-a-domain/)
|
||||
- [Create an email account](/whp/how-to/create-an-email-account/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Switching your site's backend
|
||||
description: Change the web engine (container type) running a site — standard PHP/FPM or the premium LiteSpeed/OpenLiteSpeed tier.
|
||||
sidebar:
|
||||
order: 5
|
||||
order: 6
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
@@ -41,10 +41,33 @@ Outputs one PNG per entry in `shots.config.ts` to `src/assets/screenshots/whp/<i
|
||||
|
||||
## Refresh workflow
|
||||
|
||||
UI changed? → `npm run screenshots` locally → review the diffs (`git diff --stat` shows changed PNGs) → eyeball them for accidental leakage → commit → push.
|
||||
UI changed? → run the capture (see below) locally → review the diffs (`git diff --stat` shows changed PNGs) → **open each changed PNG and eyeball it for accidental leakage** (server hostname, IP, account ID, customer domains/usernames) → commit → push.
|
||||
|
||||
- **A page covered by `shots.config.ts`** (a plain navigate-and-shoot page): `npm run screenshots`.
|
||||
- **A page that needs interaction** — opening a modal, ticking checkboxes, switching tabs — lives in a **section capture script** (see below). Re-run that script instead.
|
||||
|
||||
When a section is *reworked* (not just restyled), also: re-walk the new UI to find every state worth a screenshot, update the section script's steps, add/rename the `whp-<section>-*` ids, then refresh the `.mdx` references and run `npm run build` to confirm links and images resolve.
|
||||
|
||||
## Section capture scripts
|
||||
|
||||
`shots.config.ts` + `run.ts` only do navigate → redact → screenshot. Anything that needs **interaction or per-section redaction** gets its own `capture-<section>.ts`, run directly with `tsx`:
|
||||
|
||||
```bash
|
||||
set -a; source tools/screenshots/.env; set +a
|
||||
npx tsx tools/screenshots/capture-dns.ts
|
||||
```
|
||||
|
||||
| Script | Covers | Auth |
|
||||
| --- | --- | --- |
|
||||
| `capture-admin.ts` | Server Settings tabs, admin pages | `WHP_ADMIN_USER` |
|
||||
| `capture-site-builder.ts` | Site Builder editor states | `WHP_USER` |
|
||||
| `capture-dns.ts` | Domains & DNS list, Add Domain modal, records editor, bulk toolbar | `WHP_USER` |
|
||||
| `capture-email.ts` | Email page "Mail Client Setup" section (autodiscovery DNS records) | `WHP_USER` |
|
||||
|
||||
Each script carries its own `redact()` (text-node + input-value swaps) so fleet hostnames, IPs, and customer data become neutral placeholders while brand/demo domains stay visible. Copy the closest existing script when adding a new section — match its viewport (1440×900), `deviceScaleFactor: 2`, and **read-only** discipline (open modals and tick boxes for the shot, but never save/delete/submit).
|
||||
|
||||
## Adding a new shot
|
||||
|
||||
1. Add an entry to `shots.config.ts` with a stable `id`.
|
||||
2. `npm run screenshots`.
|
||||
1. **Static page?** Add an entry to `shots.config.ts` with a stable `id`, then `npm run screenshots`.
|
||||
2. **Interactive state?** Add the step to the relevant `capture-<section>.ts` (or copy one for a new section), then `npx tsx tools/screenshots/capture-<section>.ts`.
|
||||
3. Reference the new file in your `.mdx`: ``.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* DNS capture — the reworked "Domains & DNS" area.
|
||||
*
|
||||
* Captures, as the demo customer (so the demo domain's real zone shows):
|
||||
* - whp-domains.png the Domains & DNS list (searchable table)
|
||||
* - whp-dns-add-domain.png the Add Domain modal
|
||||
* - whp-dns-records.png the per-domain DNS Records editor
|
||||
* - whp-dns-add-record.png the inline "Add Record" editor row
|
||||
* - whp-dns-bulk-actions.png the bulk-select toolbar
|
||||
*
|
||||
* Viewport-only (1440x900), redacted for our multi-server fleet: server /
|
||||
* mail / nameserver hostnames and IPs become neutral placeholders, while the
|
||||
* brand demo domain (whp-demo.anhh.co) is kept visible on purpose.
|
||||
*
|
||||
* Read-only: opens modals and ticks checkboxes for the screenshot, never
|
||||
* saves, deletes, or submits anything.
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
|
||||
|
||||
function need(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`missing env: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const BASE = need('WHP_BASE');
|
||||
const USER = need('WHP_USER');
|
||||
const PASS = need('WHP_PASS');
|
||||
const DOMAIN = process.env.WHP_DEMO_DOMAIN ?? 'whp-demo.anhh.co';
|
||||
|
||||
const HIDE_CSS = `.navbar-text, .brand-full { visibility: hidden !important; }`;
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto(`${BASE}/login.php`, { waitUntil: 'domcontentloaded' });
|
||||
await page.fill('input[name="user"]', USER);
|
||||
await page.fill('input[name="password"]', PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Neutralise fleet-identifying text before the screenshot. The brand demo
|
||||
* domain (anhh.co) is intentionally preserved; everything that names a
|
||||
* specific server, mail host, nameserver, or IP is swapped for a placeholder.
|
||||
*/
|
||||
async function redact(page: Page) {
|
||||
await page.addStyleTag({ content: HIDE_CSS });
|
||||
await page.evaluate(() => {
|
||||
const swaps: [RegExp, string][] = [
|
||||
[/ns[12]\.whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, 'ns<n>.<your-server>.cloud-hosting.io'],
|
||||
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
||||
[/mail\d+\.cloud-hosting\.io/gi, '<mail-server>.cloud-hosting.io'],
|
||||
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
||||
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
||||
// Public IPv4 (skip RFC1918 — those read fine as generic examples)
|
||||
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
|
||||
[/demo-user/g, 'your-username'],
|
||||
];
|
||||
// Text nodes
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
const nodes: Text[] = [];
|
||||
let n: Node | null = walker.nextNode();
|
||||
while (n) { nodes.push(n as Text); n = walker.nextNode(); }
|
||||
for (const node of nodes) {
|
||||
let v = node.nodeValue ?? '';
|
||||
for (const [re, rep] of swaps) v = v.replace(re, rep);
|
||||
if (v !== node.nodeValue) node.nodeValue = v;
|
||||
}
|
||||
// Input values (the inline Add Record / TTL fields)
|
||||
document.querySelectorAll<HTMLInputElement>('input').forEach((el) => {
|
||||
if (el.type === 'password' || !el.value) return;
|
||||
let v = el.value;
|
||||
for (const [re, rep] of swaps) v = v.replace(re, rep);
|
||||
if (v !== el.value) el.value = v;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function shot(page: Page, id: string) {
|
||||
await page.waitForTimeout(400);
|
||||
await redact(page);
|
||||
const path = resolve(OUT_DIR, `${id}.png`);
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(`captured ${id} -> ${path}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await login(page);
|
||||
|
||||
// 1. Domains & DNS list
|
||||
await page.goto(`${BASE}/index.php?page=domains`, { waitUntil: 'networkidle' });
|
||||
await shot(page, 'whp-domains');
|
||||
|
||||
// 2. Add Domain modal
|
||||
await page.locator('button:has-text("Add Domain"), a:has-text("Add Domain")').first().click();
|
||||
await page.waitForTimeout(600);
|
||||
await shot(page, 'whp-dns-add-domain');
|
||||
await page.keyboard.press('Escape').catch(() => {});
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// 3. DNS Records editor for the demo domain
|
||||
await page.goto(`${BASE}/index.php?page=domains&domain=${DOMAIN}`, { waitUntil: 'networkidle' });
|
||||
await shot(page, 'whp-dns-records');
|
||||
|
||||
// 4. Inline Add Record row
|
||||
await page.locator('button:has-text("Add Record"), a:has-text("Add Record")').first().click();
|
||||
await page.waitForTimeout(500);
|
||||
await shot(page, 'whp-dns-add-record');
|
||||
// Cancel the inline add row so the next shot is clean
|
||||
await page.locator('button:has-text("Cancel")').first().click().catch(() => {});
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// 5. Bulk-select toolbar (tick two record rows)
|
||||
const rowChecks = page.locator('table tbody input[type=checkbox]');
|
||||
const n = await rowChecks.count();
|
||||
if (n >= 2) { await rowChecks.nth(0).check(); await rowChecks.nth(1).check(); }
|
||||
else if (n === 1) { await rowChecks.nth(0).check(); }
|
||||
await page.waitForTimeout(500);
|
||||
await shot(page, 'whp-dns-bulk-actions');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error(err); process.exit(1); });
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Email capture — the tabbed Email Management page, as the demo customer.
|
||||
*
|
||||
* Captures:
|
||||
* - whp-email.png the Email page on its default "Email Accounts"
|
||||
* tab: the top button strip (Webmail / Admin
|
||||
* Panel / Setup Instructions) + the tab bar
|
||||
* (Email Accounts · Forwarders · Email Domains
|
||||
* (DNS)) + the Email Accounts card.
|
||||
* - whp-email-autodiscovery.png the "Autodiscovery Records (DNS)" card on the
|
||||
* "Email Domains (DNS)" tab: the per-domain
|
||||
* autodiscovery DNS records table + copyable zone.
|
||||
*
|
||||
* Viewport-only (1440x900, deviceScaleFactor 2), redacted for our multi-server
|
||||
* fleet: server/mail hostnames + IPs become placeholders, while the brand demo
|
||||
* domain (whp-demo.anhh.co) is kept visible on purpose.
|
||||
*
|
||||
* Read-only: switches tabs / selects a domain for the shot, never saves.
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
|
||||
|
||||
function need(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`missing env: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const BASE = need('WHP_BASE');
|
||||
const USER = need('WHP_USER');
|
||||
const PASS = need('WHP_PASS');
|
||||
|
||||
const HIDE_CSS = `.navbar-text, .brand-full { visibility: hidden !important; }`;
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto(`${BASE}/login.php`, { waitUntil: 'domcontentloaded' });
|
||||
await page.fill('input[name="user"]', USER);
|
||||
await page.fill('input[name="password"]', PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Neutralise fleet-identifying text before the screenshot. The brand demo
|
||||
* domain (anhh.co) is intentionally preserved; mail/server hosts and IPs are
|
||||
* swapped for placeholders. Inline only — no named helpers inside evaluate
|
||||
* (esbuild's __name instrumentation isn't defined in the browser context).
|
||||
*/
|
||||
async function redact(page: Page) {
|
||||
await page.addStyleTag({ content: HIDE_CSS });
|
||||
await page.evaluate(() => {
|
||||
const swaps: [RegExp, string][] = [
|
||||
[/mail\d+\.cloud-hosting\.io/gi, '<mail-server>.cloud-hosting.io'],
|
||||
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
||||
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
||||
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
||||
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
|
||||
];
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
const nodes: Text[] = [];
|
||||
let n: Node | null = walker.nextNode();
|
||||
while (n) { nodes.push(n as Text); n = walker.nextNode(); }
|
||||
for (const node of nodes) {
|
||||
let v = node.nodeValue ?? '';
|
||||
for (const [re, rep] of swaps) v = v.replace(re, rep);
|
||||
if (v !== node.nodeValue) node.nodeValue = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await login(page);
|
||||
await page.goto(`${BASE}/index.php?page=email-management`, { waitUntil: 'networkidle' });
|
||||
await page.waitForSelector('#email-mgmt-nav', { state: 'visible' });
|
||||
|
||||
// --- Shot 1: the default Email Accounts tab (orientation) ---
|
||||
await page.waitForTimeout(300);
|
||||
await redact(page);
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
const mainPath = resolve(OUT_DIR, 'whp-email.png');
|
||||
await page.screenshot({ path: mainPath }); // viewport-only, no chrome
|
||||
console.log(`captured whp-email -> ${mainPath}`);
|
||||
|
||||
// --- Shot 2: the Autodiscovery Records (DNS) card on the DNS tab ---
|
||||
await page.locator('#email-mgmt-nav button[data-bs-target="#email-mgmt-dns"]').click();
|
||||
await page.waitForSelector('#custMailDnsDomain', { state: 'visible' });
|
||||
// Ensure a domain is selected, then (re)render the zone block.
|
||||
await page.evaluate(() => {
|
||||
const sel = document.getElementById('custMailDnsDomain') as HTMLSelectElement | null;
|
||||
if (sel && sel.selectedIndex < 0 && sel.options.length) sel.selectedIndex = 0;
|
||||
const fn = (window as unknown as { renderCustMailDns?: () => void }).renderCustMailDns;
|
||||
if (typeof fn === 'function') fn();
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
await redact(page); // re-run: tab content rendered after the first pass
|
||||
|
||||
// The autodiscovery section is the .card wrapping the domain <select>.
|
||||
const card = page.locator('#custMailDnsDomain').locator('xpath=ancestor::div[contains(@class,"card")][1]');
|
||||
await card.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(300);
|
||||
const autoPath = resolve(OUT_DIR, 'whp-email-autodiscovery.png');
|
||||
await card.screenshot({ path: autoPath });
|
||||
console.log(`captured whp-email-autodiscovery -> ${autoPath}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error(err); process.exit(1); });
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Traffic analytics capture — for the June 2026 platform-updates blog post.
|
||||
*
|
||||
* Captures, as the demo customer:
|
||||
* - traffic-analytics-overview.png View Traffic landing: Yesterday's Snapshot,
|
||||
* Top URLs / Bandwidth Consumers, Daily Totals
|
||||
* - traffic-analytics-day-detail.png the per-day drill-down: hourly request graph,
|
||||
* top pages by views, top pages by bandwidth
|
||||
*
|
||||
* Viewport 1440x900, deviceScaleFactor 2, fullPage:false. Redacts the fleet
|
||||
* server strip ("WHP-01" / "Welcome, ...") and the version-number footer; keeps
|
||||
* the brand demo domain (whp-demo.anhh.co) visible on purpose. Read-only.
|
||||
*
|
||||
* Output goes to /workspace/blog-assets (this is a blog image, not a KB page).
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const OUT_DIR = '/workspace/blog-assets';
|
||||
|
||||
function need(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`missing env: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const BASE = need('WHP_BASE');
|
||||
const USER = need('WHP_USER');
|
||||
const PASS = need('WHP_PASS');
|
||||
|
||||
// Hide the server-identifying navbar strip and the version footer.
|
||||
const HIDE_CSS = `.navbar-text, .brand-full, .navbar-brand { visibility: hidden !important; }`;
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto(`${BASE}/login.php`, { waitUntil: 'domcontentloaded' });
|
||||
await page.fill('input[name="user"]', USER);
|
||||
await page.fill('input[name="password"]', PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async function redact(page: Page) {
|
||||
await page.addStyleTag({ content: HIDE_CSS });
|
||||
await page.evaluate(() => {
|
||||
const swaps: [RegExp, string][] = [
|
||||
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
||||
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
||||
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
||||
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
|
||||
[/demo-user/g, 'your-username'],
|
||||
// Strip the release/version identifier from the footer.
|
||||
[/Web Hosting Panel\s*-\s*\d{4}\.\d{2}\.\d+/gi, 'Web Hosting Panel'],
|
||||
[/\b\d{4}\.\d{2}\.\d+\b/g, ''],
|
||||
];
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
const nodes: Text[] = [];
|
||||
let n: Node | null = walker.nextNode();
|
||||
while (n) { nodes.push(n as Text); n = walker.nextNode(); }
|
||||
for (const node of nodes) {
|
||||
let v = node.nodeValue ?? '';
|
||||
for (const [re, rep] of swaps) v = v.replace(re, rep);
|
||||
if (v !== node.nodeValue) node.nodeValue = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await login(page);
|
||||
|
||||
// Resolve the demo site's traffic view via the "View Traffic" button.
|
||||
await page.goto(`${BASE}/index.php?page=site-traffic`, { waitUntil: 'networkidle' });
|
||||
await page.locator('a:has-text("View Traffic"), button:has-text("View Traffic")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
const trafficUrl = page.url();
|
||||
|
||||
// 1. Overview — clip to the main content card so the snapshot + daily totals
|
||||
// frame nicely without the empty side gutters.
|
||||
await redact(page);
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: resolve(OUT_DIR, 'traffic-analytics-overview.png'), fullPage: false });
|
||||
console.log('captured traffic-analytics-overview');
|
||||
|
||||
// 2. Day drill-down — click the 23rd (richest demo data), then clip to the
|
||||
// "Breakdown for ..." card (hourly graph + top pages + bandwidth).
|
||||
await page.locator('a:has-text("2026-06-23")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1500);
|
||||
await redact(page);
|
||||
const card = page.locator('#day-detail, .card:has-text("Breakdown for")').first();
|
||||
await card.scrollIntoViewIfNeeded().catch(() => {});
|
||||
await page.waitForTimeout(400);
|
||||
if (await card.count()) {
|
||||
await card.screenshot({ path: resolve(OUT_DIR, 'traffic-analytics-day-detail.png') });
|
||||
} else {
|
||||
await page.screenshot({ path: resolve(OUT_DIR, 'traffic-analytics-day-detail.png'), fullPage: false });
|
||||
}
|
||||
console.log('captured traffic-analytics-day-detail');
|
||||
console.log('traffic url was', trafficUrl);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error(err); process.exit(1); });
|
||||