Author SHA1 Message Date
shadowdaoandClaude Sonnet 5 113c795c83 Document Node.js as a backend option when switching a site's container type
The "Switching your site's backend" page only covered PHP/PHP-FPM and the
LiteSpeed/OLS add-on tier. Node18/Node20/Node22 are also selectable in the
same Container Type dropdown, but weren't mentioned. Adds a Node.js section
covering the npm-start model, the auto-raised 512 MB memory floor, the
WebSocket/real-time checkbox, and that Node containers skip the shared
PHP-FPM/LiteSpeed placement choice — plus a capture script and screenshot
of the Edit Site modal with Node22 selected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 09:50:08 -07:00
jknapp 0e3ae941eb Merge pull request 'Add llms.txt for LLM-friendly site discovery' (#11) from add-llms-txt into main
Build and deploy / deploy (push) Successful in 31s
2026-08-03 02:51:19 +00:00
AnHonestHost DevandClaude Opus 5 1e3e0bd546 Add llms.txt for LLM-friendly site discovery
Publishes /llms.txt per the llmstxt.org convention: an H1, a summary
blockquote, an orientation paragraph, and H2 sections mirroring the
sidebar with links to every doc page (frontmatter titles and
descriptions, absolute URLs with trailing slashes), plus an Optional
section with the homepage and sitemap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:51:05 -07:00
jknapp a28e20f881 Merge pull request 'chore: version-control the WHMCS KB redirect rules' (#10) from chore/save-whmcs-redirects into main
Build and deploy / deploy (push) Successful in 27s
2026-08-02 00:28:09 +00:00
AnHonestHost DevandClaude Opus 5 063f992516 chore: version-control the WHMCS KB redirect rules
These 301s live in .htaccess on secure.anhonesthost.com and existed only
on that server. WHMCS rewrites .htaccess during some updates, which would
silently drop them — every retired KB URL would start 404ing with nothing
in version control to restore from.

deploy/README.md documents where it installs, how to reinstall, and the
two traps: article/category IDs collide (only the trailing .html tells
them apart, so rule order is load-bearing) and the slug is ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 17:27:59 -07:00
jknapp e4ea0484fd Merge pull request #9: port WHMCS KB articles to the dedicated KB
Build and deploy / deploy (push) Successful in 2m48s
Adds cPanel, Domains, Email, and Support sections so the WHMCS knowledgebase can be retired.
2026-08-01 22:11:04 +00:00
AnHonestHost DevandClaude Opus 5 b00a5fd8cf docs: add Email section — client setup + Baruwa retirement notice
Replaces the three Outlook 2016 articles (WHMCS 2, 3, 9 — ~62k views)
with one client-agnostic setup guide. Outlook 2016 went end of life in
October 2025, and the two archiving articles documented a feature the
current Outlook doesn't have.

Server settings are taken from the live autoconfig endpoint rather than
the old articles: IMAP 993/SSL, POP3 995/SSL, SMTP 587/STARTTLS, username
is the full address. cPanel settings verified against cpanel01.

Adds a Baruwa retirement notice covering the move to Proxmox Mail Gateway
in November 2026. Deliberately frames this as a platform change rather
than describing our current outbound filtering posture — see the PR
discussion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 14:51:09 -07:00
AnHonestHost DevandClaude Opus 5 425f364f48 docs: add cPanel section; drop the outbound-filtering page
Adds a cPanel hosting section so cPanel customers have a correct
destination — previously this KB was WHP-only, so cPanel-specific
articles had nowhere to redirect to:

- cpanel/nameservers                        (WHMCS 5, 24k views)
- cpanel/free-ssl-certificate               (WHMCS 8, 18k views)
- cpanel/fix-a-403-error                    (WHMCS 6, 20k views)
- cpanel/wordpress-email-from-your-domain   (WHMCS 7, 25k views)

Rewritten rather than copied where the originals were wrong or thin:
- WordPress SMTP told customers to use port 25 unencrypted; now 587/TLS
  with 465 as the implicit-TLS alternative.
- The 403 article now says explicitly that loosening permissions to 777
  makes suEXEC refuse harder, since that's the instinctive wrong fix.
- AutoSSL gained the actual failure modes (CAA records, .htaccess
  redirects intercepting validation, domain not added to the account).

Also removes email/outbound-spam-filtering, added in the previous commit.
Outbound filtering via Baruwa has been disabled and Baruwa is being retired,
so the page describes a feature we no longer offer. It needs rewriting
around the Proxmox migration before it can ship.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 14:47:44 -07:00
AnHonestHost DevandClaude Opus 5 ee6b6af107 docs: port still-relevant WHMCS KB articles to the dedicated KB
First step of retiring the WHMCS knowledgebase in favour of
kb.anhonesthost.com. Ports the four articles that are still accurate and
product-agnostic, into three new top-level sections:

- domains/  — transferring a domain, flushing a local DNS cache
- email/    — why we filter outbound mail
- support/  — remote support via RustDesk

Content was refreshed rather than copied verbatim:
- RustDesk download links were pinned to 1.3.8; now point at /releases/latest
  (current is 1.4.9). API server switched to https, which the relay serves.
- macOS DNS flush gained the killall mDNSResponder step, without which the
  documented command usually appears to do nothing.
- The nameserver list was deliberately not ported — this KB points customers
  at Dashboard → Server Information instead of hardcoding per-server values.
- Added an anti-social-engineering warning to the remote-support page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 14:36:30 -07:00
jknapp f0ec3395ab Merge pull request 'docs: link Bichon to its open-source project' (#8) from docs/bichon-opensource-link into main
Build and deploy / deploy (push) Successful in 24s
Reviewed-on: #8
2026-06-29 01:31:02 +00:00
jknapp 8c515a8074 Merge branch 'main' into docs/bichon-opensource-link 2026-06-29 01:30:55 +00:00
shadowdaoandClaude Opus 4.8 5fdf55de7f docs: link Bichon to its open-source project
The archival-email page linked to https://anhonesthost.com/bichon/, which
does not exist. Point readers at the upstream open-source project instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:29:33 -07:00
jknapp f7d739fe2d Merge pull request 'docs(how-to): add 'Clear your site's cache' guide' (#7) from docs/clear-your-cache into main
Build and deploy / deploy (push) Successful in 25s
Reviewed-on: #7
2026-06-27 04:04:58 +00:00
shadowdaoandClaude Opus 4.8 858d505e7e docs(how-to): add 'Clear your site's cache' guide
Customer-facing troubleshooting for stale content: hard refresh (browser cache)
first, then purge LiteSpeed Cache via the WP plugin Toolbox for Optimized
Webserver sites, with a note that logged-in views are never cached. Cross-linked
from the Optimized Webserver add-on page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:02:17 -07:00
jknapp 240b6d392b Merge pull request 'docs(email): fix webmail-reachability note and SMTP port guidance' (#5) from docs/email-port-webmail-fixes into main
Build and deploy / deploy (push) Successful in 22s
Reviewed-on: #5
2026-06-23 22:15:01 +00:00
jknapp 9762822e97 Merge branch 'main' into docs/email-port-webmail-fixes 2026-06-23 22:14:52 +00:00
jknapp 54bee2cf55 Merge pull request 'docs(archival): retention is for the life of the account, not 14 days' (#6) from docs/archival-retention into main
Build and deploy / deploy (push) Successful in 24s
Reviewed-on: #6
2026-06-23 22:14:46 +00:00
shadowdaoandClaude Opus 4.8 ef4a4605b2 docs(archival): correct retention — kept for the life of the account
The add-on does not impose a 14-day window or offer "configurable retention."
Archived mail is retained for as long as the customer has an active account
with us. Replaced the inaccurate "14-day quick-restore window" and
"Configurable longer retention" highlights, and softened the compliance
use-case wording away from "fixed window."

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:13:55 -07:00
shadowdaoandClaude Opus 4.8 3b31acbbac docs(email): fix webmail-reachability note and outgoing SMTP port guidance
- Webmail is hosted at our address (the Webmail button opens it directly), so
  it does not depend on the customer's domain/DNS. Replaced the incorrect
  "DNS still propagating" troubleshooting note.
- Outgoing SMTP: lead with port 465 (SSL/TLS) as the standard and present 587
  (STARTTLS) as the alternate submission port; note that port 25 is for
  server-to-server and shouldn't be used from a mail client. Updated the
  IMAP-but-not-SMTP troubleshooting entry to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:09:09 -07:00
jknapp 3a526783cb Merge pull request 'docs(email): update Create an email account for the tabbed layout' (#4) from docs/email-tabs into main
Build and deploy / deploy (push) Successful in 25s
Reviewed-on: #4
2026-06-23 21:56:46 +00:00
shadowdaoandClaude Opus 4.8 1769d5dc0b docs(email): show the create-account form in the Email Accounts screenshot
Bumped the demo account's email-account allowance so the page renders the
"Create Email Account" button + usage bar instead of the limit-reached state.
Updated steps to match the button → modal flow (Create Email Account opens the
form; the modal's submit button is "Create Account").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:54:29 -07:00
shadowdaoandClaude Opus 4.8 f1159867df docs(email): update Create an email account for the new tabbed layout
The Email page is now organized into tabs (Email Accounts / Forwarders /
Email Domains (DNS)) with a top button strip (Webmail / Admin Panel /
Setup Instructions). Reworked the how-to to match:
- orient readers to the tabs + top buttons; create on the Email Accounts tab
- autodiscovery records now live in Email Domains (DNS) → Autodiscovery
  Records (DNS) (was "Mail Client Setup")
- DKIM is in the DKIM Management section on the Email Domains (DNS) tab
- Webmail / Setup Instructions are the top-strip buttons

Recaptured whp-email.png (Email Accounts tab) and whp-email-autodiscovery.png
(DNS tab) via the rewritten capture-email.ts (clicks the DNS tab; fleet
hostnames/IPs redacted, brand demo domain kept).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:49:33 -07:00
jknapp a6269d18fd Merge pull request 'docs(screenshots): document section capture scripts' (#3) from docs/dns-page-rework into main
Build and deploy / deploy (push) Successful in 22s
Reviewed-on: #3
2026-06-22 15:43:02 +00:00
shadowdaoandClaude Opus 4.8 8f42adc799 docs(screenshots): list capture-email.ts in the section-scripts table
Completes the section-capture docs after merging main — capture-email.ts
(Email page "Mail Client Setup") now appears alongside the other scripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:41:06 -07:00
shadowdao 7f63f064c9 Merge branch 'main' into docs/dns-page-rework 2026-06-22 08:40:31 -07:00
jknapp c9a08313ca Merge pull request 'docs(email): mail-client autodiscovery + external-DNS records' (#2) from docs/mail-autodiscovery into main
Build and deploy / deploy (push) Successful in 29s
Reviewed-on: #2
2026-06-22 15:36:31 +00:00
shadowdaoandClaude Opus 4.8 0b53569821 docs(email): document mail-client autodiscovery + external-DNS records
Reflects the new customer "Mail Client Setup" section on the Email page.

- create-an-email-account.mdx: new "Auto-configure your mail app" section —
  explains clients self-configure from DNS, that domains on our nameservers
  are already set, and the records to add when DNS is hosted elsewhere
  (Cloudflare/GoDaddy/etc.) with the full RFC 6186 record table + screenshot.
  Reframe "Set up your email client" as the manual fallback.
- manage-dns-records.mdx: cross-link "Mail autodiscovery records" subsection.
- New capture-email.ts + whp-email-autodiscovery.png (fleet-redacted: mail
  host shown as <mail-server>.cloud-hosting.io; demo domain kept visible).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:11:44 -07:00
shadowdaoandClaude Opus 4.8 23cc5a887b docs(screenshots): document section capture scripts + refresh workflow
The README only covered the shots.config.ts/run.ts path. Add a Section
capture scripts table (capture-admin/site-builder/dns) and a refresh
note distinguishing static pages (npm run screenshots) from interactive
states (npx tsx capture-<section>.ts), since reworked sections need the
latter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:00:14 -07:00
jknapp 03aa273100 Merge pull request 'docs(whp): rework DNS page for new Domains & DNS UI' (#1) from docs/dns-page-rework into main
Build and deploy / deploy (push) Successful in 23s
Reviewed-on: #1
2026-06-11 21:52:34 +00:00
shadowdaoandClaude Opus 4.8 da24dc8c67 docs(whp): rework DNS page for new Domains & DNS UI
The WHP DNS area was reworked from the old Domains page (left-side add
form + right-side DNS Management dropdown) into a searchable Domains &
DNS list plus a dedicated per-domain DNS records editor.

- Recapture whp-domains.png against the new list layout
- Add screenshots for the Add Domain modal, records editor, inline
  Add Record row, and bulk-action toolbar
- Add capture-dns.ts (fleet-redacted, viewport-only) following the
  existing capture-admin.ts pattern
- Rewrite add-a-domain.mdx Add Domain steps for the modal flow and
  point at the new records editor
- Add manage-dns-records.mdx how-to (add/edit/delete, type filter,
  bulk actions, verification, troubleshooting); renumber sidebar order

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:50:21 -07:00
36 changed files with 1729 additions and 37 deletions
+20
View File
@@ -19,6 +19,10 @@ export default defineConfig({
'/whp/reference/': '/whp/reference/service-hostnames/',
'/whp/add-ons/': '/whp/add-ons/overview/',
'/whp/admin/': '/whp/admin/overview/',
'/domains/': '/domains/transferring-a-domain-to-us/',
'/support/': '/support/remote-support/',
'/cpanel/': '/cpanel/nameservers/',
'/email/': '/email/set-up-your-email-client/',
},
vite: {
resolve: {
@@ -85,6 +89,22 @@ export default defineConfig({
},
],
},
{
label: 'cPanel hosting',
items: [{ autogenerate: { directory: 'cpanel' } }],
},
{
label: 'Domains',
items: [{ autogenerate: { directory: 'domains' } }],
},
{
label: 'Email',
items: [{ autogenerate: { directory: 'email' } }],
},
{
label: 'Support',
items: [{ autogenerate: { directory: 'support' } }],
},
// Future products only appear once they have content.
],
pagefind: true,
+59
View File
@@ -0,0 +1,59 @@
# Deploy artefacts
Config that lives on other servers but belongs under version control here,
because it exists to serve this KB.
## `whmcs-kb-redirects.conf`
301 redirects that send the retired WHMCS knowledgebase
(`secure.anhonesthost.com/knowledgebase/*`) to its replacement pages on
kb.anhonesthost.com. Installed 2026-08-01.
**Where it lives in production:** `/home/whmcs/public_html/.htaccess` on
`secure.anhonesthost.com`, prepended **before** the
`### BEGIN - WHMCS managed rules ###` marker. It has to come first — WHMCS's
own block ends with a catch-all that routes everything to `index.php`, so
rules placed after it never run.
The canonical copy on that server is `/root/kb-redirects.conf`, and the
pre-migration `.htaccess` is backed up at
`/root/htaccess-backup-20260801.bak`.
### Reinstalling
WHMCS rewrites `.htaccess` during some updates, which will silently drop
these rules. To restore:
```bash
cd /home/whmcs/public_html
cp -a .htaccess /root/htaccess-backup-$(date +%Y%m%d).bak # keep whatever WHMCS wrote
cat /root/kb-redirects.conf /root/htaccess-backup-$(date +%Y%m%d).bak > /tmp/htaccess.new
install -o whmcs -g whmcs -m 644 /tmp/htaccess.new .htaccess
apachectl -t
```
Then spot-check a redirect:
```bash
curl -sI https://secure.anhonesthost.com/knowledgebase/13/x.html | grep -i location
# expect: https://kb.anhonesthost.com/support/remote-support/
```
### Two things that will bite you when editing it
**Article and category IDs collide.** Both use the shape
`/knowledgebase/<id>/<slug>`, and the same number means different things —
article 5 is "What are my Name Servers?", category 5 is "WordPress Specific".
The *only* discriminator is the trailing `.html` on articles. That's why every
article rule appears before every category rule and terminates with `[L]`: by
the time the broad category patterns run, anything ending `.html` is already
gone. Reordering the file breaks this silently, and the wrong page still
returns 200.
**The slug is ignored.** WHMCS reads only the id, so any slug with the right
id resolves. The rules match `[^/]*` for the slug for the same reason.
Legacy pre-SEO URLs (`knowledgebase.php?action=displayarticle&id=N` and
`?action=displaycat&catid=N`) are mapped explicitly. `(^|&)id=` cannot match
`catid=` — the preceding character is `t`, not `&` or start-of-string — so the
two sets can't cross-fire.
+107
View File
@@ -0,0 +1,107 @@
### BEGIN - KB migration redirects to kb.anhonesthost.com ###
# Added 2026-08-01. The WHMCS knowledgebase is retired; these 301s send its
# URLs to the equivalent page on the dedicated KB.
#
# ORDER MATTERS. Article and category URLs share the shape
# /knowledgebase/<id>/<slug>
# and their IDs COLLIDE (article 5 is "What are my Name Servers?", category 5
# is "WordPress Specific"). The only discriminator is the trailing ".html" on
# articles. Article rules therefore come first and terminate with [L]; the
# category rules below can then match broadly because anything ending .html
# has already been redirected and will never reach them.
#
# The slug is ignored by WHMCS (only the id is read), so it is ignored here
# too — any slug with the right id resolves to the right destination.
#
# The trailing "?" on each target discards the inbound query string.
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteBase /
# --- Articles (.html) -------------------------------------------------
RewriteRule ^knowledgebase/2/[^/]*\.html$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteRule ^knowledgebase/3/[^/]*\.html$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteRule ^knowledgebase/4/[^/]*\.html$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
RewriteRule ^knowledgebase/5/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
RewriteRule ^knowledgebase/6/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
RewriteRule ^knowledgebase/7/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
RewriteRule ^knowledgebase/8/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/free-ssl-certificate/? [R=301,L]
RewriteRule ^knowledgebase/9/[^/]*\.html$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteRule ^knowledgebase/10/[^/]*\.html$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
RewriteRule ^knowledgebase/11/[^/]*\.html$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
RewriteRule ^knowledgebase/12/[^/]*\.html$ https://kb.anhonesthost.com/domains/flush-your-dns-cache/? [R=301,L]
RewriteRule ^knowledgebase/13/[^/]*\.html$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
RewriteRule ^knowledgebase/14/[^/]*\.html$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
# --- Categories (no .html) --------------------------------------------
RewriteRule ^knowledgebase/2/[^/]*/?$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteRule ^knowledgebase/3/[^/]*/?$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
RewriteRule ^knowledgebase/4/[^/]*/?$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
RewriteRule ^knowledgebase/5/[^/]*/?$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
RewriteRule ^knowledgebase/6/[^/]*/?$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
RewriteRule ^knowledgebase/7/[^/]*/?$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
RewriteRule ^knowledgebase/8/[^/]*/?$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
RewriteRule ^knowledgebase/9/[^/]*/?$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
# --- Legacy query-string URLs -----------------------------------------
# Pre-SEO-URL links still in old tickets and emails:
# knowledgebase.php?action=displayarticle&id=<id>
# knowledgebase.php?action=displaycat&catid=<id>
# WHMCS used to 301 these to the friendly URL itself, but our rules above
# now intercept first — so map them explicitly or they all land on the KB
# home page and lose their destination.
#
# "(^|&)id=" cannot match "catid=" (the preceding char is "t", not & or
# start-of-string), so the two sets can't cross-fire.
RewriteCond %{QUERY_STRING} (^|&)id=2(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=3(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=4(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=5(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=6(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=7(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=8(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/free-ssl-certificate/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=9(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=10(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=11(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=12(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/flush-your-dns-cache/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=13(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)id=14(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=2(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=3(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=4(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=5(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=6(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=7(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=8(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
RewriteCond %{QUERY_STRING} (^|&)catid=9(&|$)
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
# --- Tags, search, index, and anything else under /knowledgebase ------
# Catch-all last: any KB URL not matched above lands on the KB home page
# rather than a WHMCS 404.
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/? [R=301,L]
RewriteRule ^knowledgebase(/.*)?$ https://kb.anhonesthost.com/? [R=301,L]
</IfModule>
### END - KB migration redirects to kb.anhonesthost.com ###
+83
View File
@@ -0,0 +1,83 @@
# AnHonestHost Knowledge Base
> Customer documentation for WHP (AnHonestHost's containerized Web Hosting Platform), plus cPanel hosting, domains, email, and remote support.
WHP is AnHonestHost's containerized hosting platform, and most of this knowledge base documents it: getting started, how-to guides, the Site Builder, local development with the same container images used in production, reference material, and optional add-ons. Separate sections cover legacy cPanel hosting, domain management, email setup, and remote support. Pages in the WHP Admin section document server-wide super-admin controls, which most customers won't have access to.
## WHP — Getting started
- [Welcome to WHP](https://kb.anhonesthost.com/whp/getting-started/welcome/): Quick orientation for new customers — what WHP is and where to start.
- [What is containerized hosting?](https://kb.anhonesthost.com/whp/getting-started/what-is-containerized-hosting/): Plain-language explainer of containerized hosting and how it compares to shared hosting and a VPS.
## WHP — How-to guides
- [Add a domain](https://kb.anhonesthost.com/whp/how-to/add-a-domain/): Point a domain at your WHP hosting — whether you registered it with us or elsewhere.
- [Manage DNS records](https://kb.anhonesthost.com/whp/how-to/manage-dns-records/): View, add, edit, and delete DNS records for your domains using the Domains & DNS records editor in WHP.
- [Create a site](https://kb.anhonesthost.com/whp/how-to/create-a-site/): Spin up a containerized site on a domain you've added to WHP.
- [Create an email account](https://kb.anhonesthost.com/whp/how-to/create-an-email-account/): Add a mailbox on one of your domains and connect your email client.
- [Backups](https://kb.anhonesthost.com/whp/how-to/backups/): Run on-demand and scheduled backups of your sites and databases, and confirm they're succeeding.
- [Switching your site's backend](https://kb.anhonesthost.com/whp/how-to/switching-site-backend/): Change the web engine (container type) running a site — standard PHP/FPM or the premium LiteSpeed/OpenLiteSpeed tier.
- [Clear your site's cache](https://kb.anhonesthost.com/whp/how-to/clear-your-cache/): Seeing an old version of a page after making a change? Here's how to clear cached content so your updates show up.
## WHP — Site Builder
- [Site Builder overview](https://kb.anhonesthost.com/whp/site-builder/overview/): Build a website visually inside WHP — drag-and-drop blocks, ready-made templates, draft / publish workflow.
- [Getting started](https://kb.anhonesthost.com/whp/site-builder/getting-started/): Open Site Builder, pick a template or start from scratch, make your first edits, and publish.
- [Blocks & pages](https://kb.anhonesthost.com/whp/site-builder/blocks-and-pages/): The Site Builder block library, plus how to add pages and edit the shared header and footer.
- [Styling your site](https://kb.anhonesthost.com/whp/site-builder/styling/): Set colours, fonts, and link styles once with Site Design Tokens; reach for Advanced when you need finer control.
- [Publishing & code injection](https://kb.anhonesthost.com/whp/site-builder/publishing/): Drafts vs. publish, device previews, and adding analytics, custom fonts, or global CSS via the custom head code panel.
## WHP — Local development
- [Develop locally with our containers](https://kb.anhonesthost.com/whp/local-dev/overview/): Run the same Apache/PHP and Node containers locally that we use to host your site in production.
- [PHP + Apache locally](https://kb.anhonesthost.com/whp/local-dev/php-apache/): Run the cloud-apache-container image on your laptop for WordPress and other PHP apps.
- [Node + Nginx locally](https://kb.anhonesthost.com/whp/local-dev/node/): Run the cloud-node-container image on your laptop for Express, custom Node apps, and PM2-managed processes.
## WHP — Reference
- [Service hostnames](https://kb.anhonesthost.com/whp/reference/service-hostnames/): Quick reference for connecting to MySQL, PostgreSQL, and Valkey from inside your container.
## WHP — Add-ons
- [Add-ons overview](https://kb.anhonesthost.com/whp/add-ons/overview/): Optional features you can layer on your hosting plan — monitoring, archival email, resource upgrades, and more.
- [Site Monitoring](https://kb.anhonesthost.com/whp/add-ons/monitoring/): Proactive alerts for site errors, brute-force attempts, and exploit signatures.
- [Archival email](https://kb.anhonesthost.com/whp/add-ons/archival-email/): Long-term, searchable archive of your mailbox content, separate from the live mailbox.
- [Resource upgrades](https://kb.anhonesthost.com/whp/add-ons/resource-upgrades/): Add CPU, RAM, or disk to your container without migrating to a different plan.
- [Email upgrades](https://kb.anhonesthost.com/whp/add-ons/email-upgrades/): Add mailboxes or bump per-mailbox storage on your plan.
- [Optimized Webserver (OpenLiteSpeed + LSCache)](https://kb.anhonesthost.com/whp/add-ons/optimized-webserver/): Run your sites on OpenLiteSpeed with server-level full-page caching for dramatically faster page delivery — ideal for WordPress and other dynamic CMS sites.
## WHP — Admin
- [Admin overview](https://kb.anhonesthost.com/whp/admin/overview/): What WHP super admin unlocks — server-wide controls for services, mail, DNS, security, monitoring, and users.
- [Server settings & services](https://kb.anhonesthost.com/whp/admin/server-settings/): Restart services, configure mail server, manage DNS / nameservers, HAProxy + system SSL certificates, and integration API keys.
- [Coraza WAF rules](https://kb.anhonesthost.com/whp/admin/coraza-waf/): Set the global WAF mode, tune individual rules, audit blocked requests, and add per-host overrides.
- [AI Monitor, Issues & Ignore Rules](https://kb.anhonesthost.com/whp/admin/site-monitoring/): The three admin pages that drive the Site Monitoring add-on — AI Monitor dashboard, Issues, and Ignore Rules.
- [Users & delegated access](https://kb.anhonesthost.com/whp/admin/user-management/): Create WHP users, set account types, change passwords, plus delegated user access and account suspensions.
- [Backups](https://kb.anhonesthost.com/whp/admin/backups/): How WHP's automatic backups work, the default-target requirement, full-server backups vs customer data backups, and managing backup targets.
- [Data-drive encryption (LUKS)](https://kb.anhonesthost.com/whp/admin/data-drive-encryption/): Optional LUKS2 encryption of the /docker data volume on new server installs. Encrypts customer data, databases, and container state at rest; adds a manual unlock step after every reboot.
## cPanel hosting
- [What are my nameservers?](https://kb.anhonesthost.com/cpanel/nameservers/): The nameservers to set at your registrar so your domain points at your cPanel hosting with us.
- [Get your free SSL certificate](https://kb.anhonesthost.com/cpanel/free-ssl-certificate/): Every cPanel account includes free Let's Encrypt certificates through AutoSSL. Here's what has to be in place and what to do if one doesn't issue.
- [Why is my site getting a 403 error?](https://kb.anhonesthost.com/cpanel/fix-a-403-error/): A 403 Forbidden on cPanel hosting is almost always file permissions or file ownership. Here's how to check and fix both.
- [Send WordPress email from your own domain](https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/): Stop WordPress sending as user@server and send from an address on your domain instead, using an SMTP plugin and a real mailbox.
## Domains
- [Transferring a domain to us](https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/): What happens after you start a domain transfer — the confirmation emails, how long it takes, and the nameserver check to do afterwards.
- [Flush your DNS cache](https://kb.anhonesthost.com/domains/flush-your-dns-cache/): Your site moved or its DNS changed, but your computer still loads the old version? Clear the DNS cache your device has saved locally.
## Email
- [Set up your email](https://kb.anhonesthost.com/email/set-up-your-email-client/): Connect Outlook, Apple Mail, Thunderbird, or your phone to your mailbox — with the server settings for both WHP and cPanel hosting.
- [Changes to spam filtering](https://kb.anhonesthost.com/email/spam-filtering-changes/): We're moving spam filtering from Baruwa to Proxmox Mail Gateway. What's changing, when, and what you need to do.
## Support
- [Remote support with RustDesk](https://kb.anhonesthost.com/support/remote-support/): Install and configure the RustDesk client so our support team can connect to your device and help directly.
## Optional
- [AnHonestHost KB homepage](https://kb.anhonesthost.com/): Start page for the knowledge base.
- [Sitemap](https://kb.anhonesthost.com/sitemap-index.xml): XML sitemap index for all pages on this site.
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

@@ -0,0 +1,67 @@
---
title: Why is my site getting a 403 error?
description: A 403 Forbidden on cPanel hosting is almost always file permissions or file ownership. Here's how to check and fix both.
sidebar:
order: 3
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
A **403 Forbidden** means the web server found your file but refused to serve it. On our cPanel servers that's nearly always one of two things: permissions that are wrong, or files owned by the wrong user.
We run **suEXEC**, which makes your site's code run as *your* account rather than a shared web-server user. That's a security benefit — one account can't read another's files — but it does mean the server is strict about ownership and permissions.
## The correct values
| Item | Permissions |
| --- | --- |
| Folders | `755` |
| Files | `644` |
Anything **more permissive** (a folder at `777`, a file at `666`) is refused by suEXEC. This surprises people, because loosening permissions is the instinctive fix for a permissions error — here it makes things worse, and it's a genuine security risk.
## Fixing it in cPanel
The File Manager handles this without needing a terminal:
<Steps>
1. Sign in to cPanel and open **File Manager**.
2. Navigate to the site's folder. For your primary domain that's normally `public_html`.
3. Select the folder or file, then click **Permissions** in the toolbar.
4. Set folders to `755` and files to `644`. To apply to everything beneath a folder at once, tick the **recurse into subdirectories** option and choose whether it applies to files or folders — you'll need one pass for each, since they take different values.
</Steps>
## When ownership is the problem
Permissions are only half of it. Files also have to be owned by your account.
- **Uploaded over FTP or SFTP?** Ownership is almost certainly fine — the files were created as you.
- **Pulled the site down over SSH** with `git clone`, `wget`, `curl`, or unpacked an archive as another user? Ownership may be wrong, and no amount of `chmod` will fix that. The files need `chown` back to your account.
If you suspect ownership, [open a ticket](https://secure.anhonesthost.com/submitticket.php) — it's a one-command fix from our side and safer than guessing.
<Aside type="caution">
Be careful with recursive commands over SSH. A `chmod -R 777` or a `chown` aimed at the wrong path can break your whole account, and in the case of `777` will leave the site returning 403 anyway. If you aren't confident, ask us.
</Aside>
## If permissions and ownership are both correct
Other causes of a 403:
- **No index file.** A folder with no `index.html` or `index.php`, on a server with directory listing disabled, returns 403.
- **An `.htaccess` rule** denying access — either one you added, or one a security plugin wrote.
- **A security plugin or firewall** blocking your IP after failed logins. Try from a different network or your phone on mobile data; if that works, it's an IP block.
## Related
- [Get your free SSL certificate](/cpanel/free-ssl-certificate/)
## Still stuck?
<Support />
@@ -0,0 +1,58 @@
---
title: Get your free SSL certificate
description: Every cPanel account includes free Let's Encrypt certificates through AutoSSL. Here's what has to be in place and what to do if one doesn't issue.
sidebar:
order: 2
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
Your cPanel hosting includes free SSL certificates from **Let's Encrypt**, issued automatically by cPanel's AutoSSL service. There's nothing to buy and nothing to install by hand.
## What has to be true first
AutoSSL can only issue a certificate once both of these are in place:
<Steps>
1. **The domain is added to your cPanel account** — as the primary domain, an addon domain, or a subdomain.
2. **DNS points at our server.** Let's Encrypt validates a certificate by fetching a file from wherever the domain currently resolves. If it still points at your old host, validation fails and no certificate is issued.
</Steps>
Check where a domain currently resolves at [whatsmydns.net](https://www.whatsmydns.net/).
## When it issues
AutoSSL runs on a schedule — **once every 24 hours**. So after adding a domain and pointing DNS at us, a certificate normally appears within a day without you doing anything.
Renewals are automatic too. Let's Encrypt certificates are valid for 90 days and AutoSSL renews them well before expiry, so a working site stays working.
<Aside type="tip">
Need it sooner than the next daily run? Confirm DNS is pointing at us, then [open a ticket](https://secure.anhonesthost.com/submitticket.php) — we can trigger AutoSSL by hand and usually have the certificate in place shortly after.
</Aside>
## If a certificate doesn't appear
Work through these in order — the first two cover most cases:
- **DNS isn't pointing here yet.** The most common cause by far. Verify at whatsmydns.net that the domain resolves to our server's IP.
- **It hasn't been 24 hours.** Give the scheduled run a chance before assuming something is broken.
- **The domain isn't actually in your account.** Check cPanel → **Domains**. A domain you own but haven't added is invisible to AutoSSL.
- **A redirect is intercepting validation.** Forced redirects — especially domain-wide ones in `.htaccess` — can stop Let's Encrypt from reaching the validation file.
- **CAA records are blocking issuance.** If your DNS has a CAA record naming a different certificate authority, Let's Encrypt is refused. It must permit `letsencrypt.org`.
## Making your site actually use it
An issued certificate doesn't automatically mean visitors get HTTPS. Once it's in place, make sure your site loads over `https://` and redirects visitors from `http://`. In WordPress, that's usually setting both the **WordPress Address** and **Site Address** to the `https://` version under **Settings → General**.
## Related
- [What are my nameservers?](/cpanel/nameservers/)
- [Why is my site getting a 403 error?](/cpanel/fix-a-403-error/)
## Still stuck?
<Support />
+49
View File
@@ -0,0 +1,49 @@
---
title: What are my nameservers?
description: The nameservers to set at your registrar so your domain points at your cPanel hosting with us.
sidebar:
order: 1
---
import { Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
Nameservers tell the internet which company is in charge of your domain's DNS. To use your hosting with us, set these at whichever registrar your domain is registered with.
## Shared and reseller hosting (cPanel01)
```text
ns1.cpanel01.cloud-hosting.io
ns2.cpanel01.cloud-hosting.io
```
Both shared and reseller accounts on cPanel01 use the same pair.
<Aside type="note">
**On WHP instead?** WHP servers each have their own nameservers, so there's no single pair to publish here. Yours are shown in WHP on the **Dashboard** page under **Server Information** — see [Add a domain](/whp/how-to/add-a-domain/).
</Aside>
## Setting them
Nameservers are changed at your **registrar** (whoever you bought the domain from), not in cPanel. Look for a section called *Nameservers*, *DNS*, or *Domain settings*, choose the "custom nameservers" option, and enter both values above.
If your domain is registered with us, tell us the change you need and we'll make it for you.
## How long does it take?
Nameserver changes propagate across the internet gradually — usually within a few hours, occasionally up to 48. You can watch progress at [whatsmydns.net](https://www.whatsmydns.net/).
During that window some visitors reach the new server while others still reach the old one. That's expected, and it's why we recommend leaving your old hosting active until propagation finishes.
<Aside type="caution">
Changing nameservers moves **all** of your DNS to us — website, email, and anything else. If your email is hosted somewhere other than your website, tell us before you switch so we can recreate those mail records here first. Otherwise mail delivery stops when the change takes effect.
</Aside>
## Related
- [Flush your DNS cache](/domains/flush-your-dns-cache/) — if you still see the old site after propagation.
- [Transferring a domain to us](/domains/transferring-a-domain-to-us/)
## Still stuck?
<Support />
@@ -0,0 +1,74 @@
---
title: Send WordPress email from your own domain
description: Stop WordPress sending as user@server and send from an address on your domain instead, using an SMTP plugin and a real mailbox.
sidebar:
order: 4
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
If your WordPress site sends mail — contact forms, order confirmations, password resets — you may notice it arrives from something like `user@host.server-host.tld` rather than your own domain.
That's deliberate. PHP's built-in mail function is configured to send as the account that owns the site, which makes it much harder for a compromised script to forge mail as someone else. The side effect is unbranded — and often poorly delivered — email.
The fix is to send through a real mailbox on your domain using SMTP.
<Aside type="tip">
This also improves **deliverability**. Mail sent through an authenticated mailbox on your domain passes SPF and DKIM checks; mail sent by PHP as `user@server` frequently doesn't, and lands in spam.
</Aside>
## Step 1 — Create a mailbox to send from
<Steps>
1. In cPanel, open **Email Accounts** under the **Email** section.
2. Create an account for the site to send as — `noreply@yourdomain.com` is the usual choice.
3. Use the **password generator** and copy the password somewhere temporarily — you'll need it in a moment, and you won't be shown it again.
4. Set a **mailbox quota**. People reply to `noreply` addresses regardless of the name, and without a quota those replies accumulate against your hosting space indefinitely.
</Steps>
## Step 2 — Install an SMTP plugin
WordPress needs a plugin to route mail through SMTP instead of PHP. Any of the well-maintained ones work:
- [WP Mail SMTP](https://wordpress.org/plugins/wp-mail-smtp/)
- [Easy WP SMTP](https://wordpress.org/plugins/easy-wp-smtp/)
- [Post SMTP](https://wordpress.org/plugins/post-smtp/)
## Step 3 — Configure it
In the plugin's settings, choose the **Other SMTP** / custom option and enter:
| Setting | Value |
| --- | --- |
| **SMTP host** | Your server's hostname (for example `cpanel01.cloud-hosting.io`) |
| **Encryption** | TLS |
| **Port** | `587` |
| **Authentication** | On |
| **Username** | The **full** email address — `noreply@yourdomain.com`, not `noreply` |
| **Password** | The password you generated |
| **From address** | The same mailbox address |
<Aside type="caution">
Use port **587 with TLS**, or **465 with SSL** if your plugin prefers implicit TLS. Don't use port **25** without encryption — it sends your mailbox password across the network in the clear, and many networks block it outright.
</Aside>
Two details that account for most failures: the username has to be the **whole** address, and the **From** address must match the mailbox you authenticated as. A mismatch gets rejected or treated as spoofing.
## Step 4 — Send a test
Every one of these plugins has a test-email feature. Use it before assuming it works, and send to an address at a different provider (Gmail, Outlook) rather than another mailbox on your own domain — that exercises the path real recipients take.
## Related
- [What are my nameservers?](/cpanel/nameservers/)
- **On WHP?** See [Create an email account](/whp/how-to/create-an-email-account/) — the mailbox part differs, the plugin setup is the same.
## Still stuck?
<Support />
@@ -0,0 +1,80 @@
---
title: Flush your DNS cache
description: Your site moved or its DNS changed, but your computer still loads the old version? Clear the DNS cache your device has saved locally.
sidebar:
order: 2
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
When you visit a site, your computer saves ("caches") the answer to *which server is this domain on?* so it doesn't have to ask again every time. That's normally invisible and helpful — but right after a domain moves to a new server or its DNS records change, your device can keep using the old answer and show you the old site.
Devices are supposed to re-check after a short while, but some record lifetimes stretch to hours or even days. Clearing the cache by hand skips the wait.
## First, confirm it's actually your computer
Before changing anything locally, check whether the new DNS has actually gone out to the world. Look your domain up at [whatsmydns.net](https://www.whatsmydns.net/) — it queries servers in many countries at once.
- **Most locations show the new value** — the change has propagated, and a stale local cache is the likely culprit. Continue below.
- **Most locations still show the old value** — the change hasn't propagated yet. Flushing your own cache won't help; give it time.
<Aside type="tip">
A quick sanity check: open the site in a **private / incognito window**, or on your phone using mobile data instead of Wi-Fi. If it looks correct there, the problem is local to your computer.
</Aside>
## Windows
<Steps>
1. Press the **Windows key**, type `Command Prompt`, and open it.
2. Type this and press **Enter**:
```text
ipconfig /flushdns
```
3. You should see *"Successfully flushed the DNS Resolver Cache."*
</Steps>
## macOS
<Steps>
1. Open **Terminal** (in **Applications → Utilities**, or press `Cmd` + `Space` and search for "Terminal").
2. Type this and press **Enter**:
```bash
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
```
3. Enter your administrator password when prompted, then press **Enter**. Nothing is printed when it works — no news is good news.
</Steps>
<Aside type="note">
Both commands matter on macOS: the first clears the lookup cache, the second restarts the service that answers DNS queries. Running only the first often appears to do nothing.
</Aside>
## Still seeing the old site?
Your computer isn't the only thing that caches DNS. Work outward:
- **Your browser** keeps its own cache. Do a hard refresh (`Ctrl` + `Shift` + `R`, or `Cmd` + `Shift` + `R` on a Mac) or try a private window.
- **Your router** caches DNS too. Restarting it clears that.
- **Your internet provider's resolvers** cache as well, and you can't clear those — they expire on their own schedule. If everything else is clean, this is usually what's left. It typically resolves within a few hours.
If it's still wrong well after the record's lifetime should have expired, it may not be a caching problem at all — get in touch and we'll look at the actual DNS records.
## Related
- [Manage DNS records](/whp/how-to/manage-dns-records/)
- [Add a domain](/whp/how-to/add-a-domain/)
- [Clear your site's cache](/whp/how-to/clear-your-cache/) — for when *content* is stale rather than DNS.
## Still stuck?
<Support />
@@ -0,0 +1,60 @@
---
title: Transferring a domain to us
description: What happens after you start a domain transfer — the confirmation emails, how long it takes, and the nameserver check to do afterwards.
sidebar:
order: 1
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
You've started a domain transfer to us — here's what happens next, and the one thing worth checking once it lands.
## Before the transfer will go through
Your current registrar has to let the domain go first. At the registrar you're leaving, make sure you've:
- **Unlocked the domain.** Registrars set a transfer lock by default to prevent unauthorised moves.
- **Turned off WHOIS / domain privacy.** Privacy services can hide the administrative address the confirmation email needs to reach.
- **Got the EPP code** (sometimes called the auth code or transfer key). Your registrar provides this, usually by email or in the domain's settings.
<Aside type="caution">
Domains can't be transferred within **60 days** of being registered, or within 60 days of a previous transfer. That's an ICANN rule, not a registrar policy — nobody can waive it.
</Aside>
## Confirming the transfer
<Steps>
1. **Watch for confirmation emails.** You'll get one or both of:
- a message from **your previous registrar**, confirming the domain is moving away from them;
- a message from **NameCheap**, who we use to resell domains, confirming it's moving to our account.
They go to the domain's registered contact address — which may not be the address you use with us. If nothing arrives, check that address and its spam folder.
2. **Click the confirmation link** in the email. Nothing moves until you do; an unconfirmed transfer just sits until it expires.
3. **Wait for it to complete.** Once confirmed, the transfer usually finishes in **1 to 12 hours**, depending on how quickly the losing registrar releases it. Your account page updates to reflect the domain once it's done.
</Steps>
## After it completes: check your nameservers
This is the step people miss. Some registrars — **GoDaddy** most notably — reset a domain's nameservers when they release it. If that happens, the domain transfers to us successfully but still points at the old host, so your site or email can appear to break for no obvious reason.
Once the transfer shows as complete, confirm the domain's nameservers match the ones for your hosting. If you're on WHP, your server's nameservers are listed on the **Dashboard** page under **Server Information** — see [Add a domain](/whp/how-to/add-a-domain/) for where to set them.
<Aside type="tip">
A transfer moves *registration*, not *hosting*. Your site's files and email stay wherever they already live. If you're also moving hosting to us, that's a separate step — ask us and we'll help sequence the two so nothing goes dark.
</Aside>
## Related
- [Add a domain](/whp/how-to/add-a-domain/)
- [Manage DNS records](/whp/how-to/manage-dns-records/)
- [Flush your DNS cache](/domains/flush-your-dns-cache/)
## Still stuck?
<Support />
@@ -0,0 +1,91 @@
---
title: Set up your email
description: Connect Outlook, Apple Mail, Thunderbird, or your phone to your mailbox — with the server settings for both WHP and cPanel hosting.
sidebar:
order: 1
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
You can read your mail in any standard email app. Most will configure themselves once you enter your address and password — and if yours doesn't, the settings are below.
<Aside type="tip">
**Just want to check your mail?** You don't need to set anything up. Webmail works in any browser with no configuration — ask us for your webmail address if you don't have it.
</Aside>
## Let your app configure itself
Try this first. It works in most modern clients, including Outlook, Apple Mail, and the mail apps on iOS and Android.
<Steps>
1. Add a new account in your email app.
2. Enter your **full email address** (`you@yourdomain.com`) and its password.
3. Choose **Next** or **Sign in** and let the app look up the settings.
</Steps>
If the app finds everything, you're done. If it asks for server details, use the table below.
## Server settings
Use your **full email address** as the username — not just the part before the `@`. That single detail accounts for most setup failures.
### WHP hosting
| Setting | Value |
| --- | --- |
| **Incoming (IMAP)** | `mail01.cloud-hosting.io`, port **993**, SSL/TLS |
| **Incoming (POP3)** | `mail01.cloud-hosting.io`, port **995**, SSL/TLS |
| **Outgoing (SMTP)** | `mail01.cloud-hosting.io`, port **587**, STARTTLS |
| **Username** | Your full email address |
| **Authentication** | Required, for outgoing as well as incoming |
<Aside type="note">
`mail01` is an example. Your server's mail hostname is shown in WHP on the **Dashboard** page under **Server Information**, and the **Email** page has a **Setup Instructions** button with the exact values for your account.
</Aside>
### cPanel hosting
| Setting | Value |
| --- | --- |
| **Incoming (IMAP)** | `cpanel01.cloud-hosting.io`, port **993**, SSL/TLS |
| **Incoming (POP3)** | `cpanel01.cloud-hosting.io`, port **995**, SSL/TLS |
| **Outgoing (SMTP)** | `cpanel01.cloud-hosting.io`, port **465**, SSL/TLS |
| **Username** | Your full email address |
| **Authentication** | Required, for outgoing as well as incoming |
Port **587 with STARTTLS** also works for outgoing if your app prefers it.
## IMAP or POP3?
Choose **IMAP** unless you have a specific reason not to.
- **IMAP** keeps mail on the server and mirrors it to every device. Read a message on your phone and it shows as read on your laptop. This is what you want when you use more than one device.
- **POP3** downloads mail to one device and, by default, removes it from the server. Mail then exists only on that machine — if it dies, so does your mail.
<Aside type="caution">
Take care switching an existing account from POP3 to IMAP. If POP3 has been deleting messages from the server as it downloaded them, those messages exist only in your local app — and they won't reappear when you switch. Back up first, or ask us to check before you change anything.
</Aside>
## Common problems
**Incoming works, outgoing fails.** Almost always because outgoing authentication is off. Many apps leave it unticked by default. Find the option — usually *My outgoing server requires authentication* — and enable it with the same username and password.
**Password rejected.** Use the full email address as the username. If it still fails, reset the mailbox password and try again — and watch for autocorrect capitalising the first letter on phones.
**Certificate warnings.** Connect using the server hostname in the table above, not your own domain. A certificate is issued for the server's name, so connecting as `mail.yourdomain.com` can trigger a mismatch warning even though everything works.
**Old mail missing after setup.** If the account was previously POP3, see the caution above before assuming anything is lost.
## Related
- [Create an email account](/whp/how-to/create-an-email-account/) — WHP
- [Send WordPress email from your own domain](/cpanel/wordpress-email-from-your-domain/)
## Still stuck?
<Support />
@@ -0,0 +1,53 @@
---
title: Changes to spam filtering
description: We're moving spam filtering from Baruwa to Proxmox Mail Gateway. What's changing, when, and what you need to do.
sidebar:
order: 2
badge: Notice
---
import { Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
We're changing the platform that filters spam for our mail customers.
**Baruwa**, which we've used for years, reaches end of life in **November 2026** — its developers are stopping support and security updates. Rather than run mail filtering on software that no longer receives fixes, we're moving to **Proxmox Mail Gateway**.
## What this means for you
**For most customers, nothing changes and there's nothing to do.** Your address stays the same, your mailbox and its contents are untouched, and your email app keeps working without reconfiguration. Filtering continues throughout — we're changing what does it, not whether it happens.
If you're one of the customers who uses the Baruwa web interface directly — to review quarantined mail, or to manage your own allow and block lists — that's the part that changes, since Proxmox Mail Gateway has its own interface. **We'll contact you individually** to move you across and show you the equivalent screens. You don't need to do anything in the meantime.
<Aside type="tip">
Worth doing now, whoever you are: if you've built up allow or block lists in Baruwa that matter to you, take a copy. We'll migrate what we can, but having your own record makes it easy to confirm nothing was missed.
</Aside>
## Timeline
| When | What happens |
| --- | --- |
| Now | Baruwa continues to run. We begin moving customers across. |
| Before November 2026 | Affected customers are contacted individually and migrated. |
| November 2026 | Baruwa reaches end of life and is retired. |
We'll update this page as the migration progresses.
## Questions we expect
**Will I lose quarantined mail?** Anything currently held in quarantine stays available until your migration. If something in there matters, release it to your inbox rather than leaving it quarantined.
**Do I need to change my email settings?** No. Server names, ports, and passwords are unaffected — see [Set up your email](/email/set-up-your-email-client/) if you're configuring a new device.
**Is my mail less protected during the change?** No. Filtering runs continuously through the migration; there's no window where mail is unfiltered.
**I'd rather opt out of filtering entirely.** Talk to us and we'll go through the options and the trade-offs.
## Related
- [Set up your email](/email/set-up-your-email-client/)
- [Create an email account](/whp/how-to/create-an-email-account/)
## Questions about your account?
<Support />
@@ -0,0 +1,68 @@
---
title: Remote support with RustDesk
description: Install and configure the RustDesk client so our support team can connect to your device and help directly.
sidebar:
order: 1
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
Sometimes the fastest way to sort something out is for us to see your screen — configuring an email client, reproducing an error we can't recreate from our side, or walking through a setting together.
We use **RustDesk**, an open-source remote-desktop tool, running on our own relay server rather than a third-party service. Nothing is installed permanently and nothing runs in the background: you start it when you want help and close it when you're done.
<Aside type="caution">
We will never ask you to install remote-support software out of the blue. If someone contacts you claiming to be from AnHonestHost and asks for remote access to your computer — especially about a payment, a refund, or a "problem with your account" — stop and [open a ticket](https://secure.anhonesthost.com/submitticket.php) to check it's really us. Only ever run this at the end of a conversation *you* started with us.
</Aside>
## Install the client
Download RustDesk for your device from the official releases:
- **Windows, macOS, and Android** — [github.com/rustdesk/rustdesk/releases/latest](https://github.com/rustdesk/rustdesk/releases/latest). Pick the `.msi` for Windows, the `.dmg` for macOS, or the signed `.apk` for Android.
- **iOS / iPadOS** — [RustDesk on the App Store](https://apps.apple.com/us/app/rustdesk-remote-desktop/id1581225015).
## Point it at our server
We run our own ID and relay server, which is faster and keeps your session off shared public infrastructure. You only need to do this once.
<Steps>
1. Open **Settings**. On desktop that's the **three dots** next to your ID; on mobile it's the settings icon.
2. On desktop, choose **Network**. (On mobile, skip straight to the next step.)
3. Choose **ID/Relay server** and enter:
| Field | Value |
| --- | --- |
| **ID server** | `rustdesk.cloud-hosting.io` |
| **Relay server** | `rustdesk.cloud-hosting.io` |
| **API server** | `https://rustdesk.cloud-hosting.io` |
| **Key** | `UmpkeFe76AKt8vw8Pj0YCSbxIcYLsqqfPGYzLRl+PgA=` |
4. **Save**, then go back to the **Home** screen.
</Steps>
## Starting a session
The support operator needs two things from the Home screen:
- **Your ID** — a nine-digit number that stays the same for your device.
- **Your one-time password** — shown beneath the ID, and regenerated each time.
Give both to your support operator over the phone, or in your ticket.
<Aside type="tip">
If you send them in a ticket rather than over the phone, **leave your computer on with RustDesk running** — the one-time password only works while the client is open, and we can't connect to a sleeping machine.
</Aside>
## Ending a session
Close the RustDesk window when you're finished. With it closed, no connection is possible — the one-time password from your session is already spent, and a new one is generated next time you open it. You can uninstall the client entirely if you'd rather.
## Still stuck?
<Support />
@@ -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.
+10 -8
View File
@@ -27,24 +27,25 @@ import Support from '~/content/partials/support-link.mdx';
<Steps>
1. In the sidebar, click **Domains**.
![WHP Domains page](~/assets/screenshots/whp/whp-domains.png)
1. In the sidebar, click **Domains & DNS**. You'll see a searchable list of the domains on your account.
![WHP Domains & DNS page](~/assets/screenshots/whp/whp-domains.png)
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.`
![Add Domain dialog](~/assets/screenshots/whp/whp-dns-add-domain.png)
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?
+1 -1
View File
@@ -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**.
![WHP Email Management page](~/assets/screenshots/whp/whp-email.png)
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.
![The WHP Email page on the Email Accounts tab, showing the tab bar and the top access buttons](~/assets/screenshots/whp/whp-email.png)
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.
![The Autodiscovery Records (DNS) section on the Email Domains (DNS) tab, showing autodiscovery DNS records for a domain](~/assets/screenshots/whp/whp-email-autodiscovery.png)
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**.
![WHP Domains & DNS page](~/assets/screenshots/whp/whp-domains.png)
2. Find your domain in the list and click **Manage DNS**.
![DNS records editor for a domain](~/assets/screenshots/whp/whp-dns-records.png)
</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.
![Inline Add Record row](~/assets/screenshots/whp/whp-dns-add-record.png)
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:
![Bulk actions on selected DNS records](~/assets/screenshots/whp/whp-dns-bulk-actions.png)
- **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 />
@@ -1,15 +1,15 @@
---
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.
description: Change the container type running a site — standard PHP/FPM, the premium LiteSpeed/OpenLiteSpeed tier, or Node.js.
sidebar:
order: 5
order: 6
---
import { Steps, Aside } from '@astrojs/starlight/components';
import SignIn from '~/content/partials/signing-in.mdx';
import Support from '~/content/partials/support-link.mdx';
Every WHP site runs inside a container. The **container type** determines the web engine that serves your site's files and runs your PHP code. Most sites use a standard PHP or PHP-FPM container, which handles the vast majority of WordPress and PHP workloads well. If you've enabled the **Optimized Webserver** add-on, you also have access to LiteSpeed/OpenLiteSpeed (OLS) container types — a premium engine known for its built-in full-page cache (LSCache) and lower memory usage under traffic.
Every WHP site runs inside a container. The **container type** determines the engine that serves your site: PHP, LiteSpeed, or Node.js. Most sites use a standard PHP or PHP-FPM container, which handles the vast majority of WordPress and PHP workloads well. If you've enabled the **Optimized Webserver** add-on, you also have access to LiteSpeed/OpenLiteSpeed (OLS) container types — a premium engine known for its built-in full-page cache (LSCache) and lower memory usage under traffic. If you're deploying your own application code instead of PHP, Node.js container types are also available — see [Node.js](#nodejs) below.
Switching backends is a one-step change in the Sites editor, but it does briefly restart your container, so plan for a few seconds of downtime.
@@ -36,6 +36,7 @@ Switching backends is a one-step change in the Sites editor, but it does briefly
4. Select the backend you want:
- **PHP** or **PHP-FPM** options — standard shared webserver tier, suitable for most WordPress and PHP sites.
- **LiteSpeed PHP** options (e.g. *LiteSpeed PHP 8.x*) — premium OLS tier. These only appear if the Optimized Webserver add-on is active on your account.
- **Node18**, **Node20**, or **Node22** — for sites running your own Node.js application code instead of PHP. See [Node.js](#nodejs) below before switching to one of these.
5. Click **Save**. WHP recreates the container with the new engine. Expect a brief moment of downtime (typically a few seconds) while the container restarts.
@@ -55,6 +56,24 @@ To enable it:
2. Enable it from your [client portal](https://secure.anhonesthost.com/clientarea.php).
3. Once active, the LiteSpeed PHP options will appear in the Container Type dropdown when editing any site.
## Node.js
Node.js container types (**Node18**, **Node20**, **Node22**) run your own application code instead of PHP — the container starts your app the same way you would locally with `npm start`. They're for React/Next.js servers, Express APIs, and other Node apps, not for WordPress or PHP sites.
<Aside type="caution">
Switching an **existing PHP or WordPress site** to a Node container type won't make your PHP files run — Node containers don't execute PHP at all. Node backends are for sites where you're deploying your own Node.js application code. If you're starting a brand-new Node app rather than switching an existing site, [Create a site](/whp/how-to/create-a-site/) and choose **Node.js app** under "What are you building?" is the more direct path.
</Aside>
A few things behave differently for Node compared to switching between PHP tiers:
- **No PHP-FPM / LiteSpeed choice.** Node containers bundle their own web server, so the shared-vs-standalone placement option that applies to PHP and LiteSpeed containers doesn't apply here — you won't see that toggle once a Node container type is selected.
- **Higher minimum memory.** Node containers need more headroom than PHP containers. Selecting a Node container type in the editor bumps **Memory per Container** up to at least 512 MB (versus 256 MB for standard PHP). If your account doesn't have that much unused memory in its resource allowance, WHP will tell you and block the change until you free up resources or [upgrade your plan](/whp/add-ons/resource-upgrades/).
- **Optional WebSocket / real-time support.** The Edit Site form has a **WebSocket / real-time support** checkbox alongside the Container Type field. Turn it on if your Node app holds connections open (Socket.IO, live chat, streaming) — it keeps connections alive for up to 6 hours instead of the standard 5-minute timeout. This is unrelated to the container type itself and works whether or not you just switched to Node.
![Edit Site modal with Container Type set to Node22, showing Memory per Container automatically raised to 512 MB](~/assets/screenshots/whp/whp-sites-edit-node.png)
Once you've switched and saved, upload your app to the `app/` folder for that domain via SFTP — see [Create a site](/whp/how-to/create-a-site/#where-your-files-go) for the file layout and how WHP starts your app.
## Before cancelling the Optimized Webserver add-on
<Aside type="caution">
@@ -89,6 +108,10 @@ Once all sites are on standard backends, you can cancel or disable the add-on fr
**LiteSpeed cache not serving cached pages after the switch.** This is expected — the cache starts empty after every container recreation. It warms up automatically as visitors load pages.
**"Your account does not have enough resources left" when selecting a Node type.** Node containers need at least 512 MB of memory. Free up resources by removing or resizing another site, or [upgrade your plan](/whp/add-ons/resource-upgrades/).
**Site shows a 502 after switching to Node.** Give the container 15–30 seconds to start your app. If it's still down, confirm your app listens on the port WHP passes it via environment variable — see [Create a site](/whp/how-to/create-a-site/#troubleshooting).
## Related
- [Create a site](/whp/how-to/create-a-site/)
+19 -4
View File
@@ -23,10 +23,25 @@ const PRODUCT_META: Record<string, { title: string; blurb: string; firstSection:
blurb: 'Tips and tricks for getting the most out of WordPress on WHP.',
firstSection: 'index',
},
'email-clients': {
title: 'Email clients',
blurb: 'Configure Outlook, Apple Mail, Thunderbird, and mobile clients.',
firstSection: 'index',
email: {
title: 'Email',
blurb: 'Set up Outlook, Apple Mail, Thunderbird, and mobile clients — plus news about our filtering.',
firstSection: 'set-up-your-email-client',
},
domains: {
title: 'Domains',
blurb: 'Transfer a domain to us, point it at your hosting, and sort out DNS problems.',
firstSection: 'transferring-a-domain-to-us',
},
cpanel: {
title: 'cPanel hosting',
blurb: 'Nameservers, SSL certificates, permissions, and email for our cPanel shared and reseller plans.',
firstSection: 'nameservers',
},
support: {
title: 'Support',
blurb: 'Working with our support team, including remote-assistance sessions.',
firstSection: 'remote-support',
},
};
+26 -3
View File
@@ -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`: `![Alt text](~/assets/screenshots/whp/<id>.png)`.
+140
View File
@@ -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); });
+123
View File
@@ -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); });
+156
View File
@@ -0,0 +1,156 @@
/**
* Sites "backend" capture — the Edit Site modal's Container Type field,
* captured with a Node.js container type selected (for the "switching your
* site's backend" how-to's Node.js section).
*
* Captures, as the demo customer:
* - whp-sites-edit-node.png Edit Site modal, Container Type = Node22,
* showing the auto-adjusted Memory per
* Container / Total Resources readout.
*
* Viewport-only (1440x900), redacted for our multi-server fleet: server /
* mail / nameserver hostnames, IPs, and the navbar brand strip become
* neutral or hidden, while the brand demo domain (whp-demo.anhh.co) stays
* visible on purpose.
*
* Read-only: opens the Edit modal and changes the Container Type dropdown
* for the shot, but never clicks Save.
*/
import { chromium, type Page } from 'playwright';
import { mkdir, readFile } 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');
async function loadEnv(): Promise<void> {
const envPath = resolve(__dirname, '.env');
const content = await readFile(envPath, 'utf-8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
const val = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = val;
}
}
function need(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`missing env: ${name}`);
return v;
}
const HIDE_CSS = `.navbar-text, .brand-full { visibility: hidden !important; }`;
async function login(page: Page, base: string, user: string, pass: string) {
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'],
];
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;
}
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 loadEnv();
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';
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, BASE, USER, PASS);
await page.goto(`${BASE}/index.php?page=sites`, { waitUntil: 'networkidle' });
// Select the demo site from the "Select a Site" widget (Choices.js).
const siteChoicesContainer = page.locator('div.choices:has(#siteDropdown)').first();
await siteChoicesContainer.click();
await page.waitForTimeout(300);
const item = page
.locator('.choices__list--dropdown .choices__item--choice', { hasText: DOMAIN.split('.')[0] })
.first();
await item.click();
// Wait for the async site-details fetch, then open Edit (Manage).
await page.locator('#siteActionsContainer').waitFor({ state: 'visible', timeout: 10000 });
await page.waitForTimeout(300);
await page.locator('#manageSiteBtn').click();
await page.waitForTimeout(800);
// Switch the Container Type to Node22 (the newest of the three Node
// options) to show the memory floor auto-adjust to 512 MB.
const editSelect = page.locator('#edit_container_type_id');
await editSelect.waitFor({ state: 'visible', timeout: 10000 });
await editSelect.scrollIntoViewIfNeeded();
const nodeOption = editSelect.locator('option', { hasText: 'Node22' });
const nodeValue = await nodeOption.getAttribute('value');
if (!nodeValue) throw new Error('Node22 option not found in edit_container_type_id');
await editSelect.selectOption(nodeValue);
await page.waitForTimeout(600);
await shot(page, 'whp-sites-edit-node');
} finally {
await browser.close();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+116
View File
@@ -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); });