> For the complete documentation index, see [llms.txt](https://docs.layeronecloud.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.layeronecloud.com/platform/public-site.md).

# Sites, public website and status

sites.Site (name, slug, status, timezone, active flag) is the multi-site record.

## 2.1 Sites <a href="#id-21-sites" id="id-21-sites"></a>

`sites.Site` (name, slug, status, timezone, active flag) is the multi-site record. The `current_site` context processor injects `site_name`, `site_slug` and `client_portal_url` into **every** template; templates rely on it. Console site selection uses `?site=`; public status uses a route parameter. Seeded by `manage.py seed_initial_site`, which is idempotent and takes a PostgreSQL advisory lock.

## 2.2 CMS pages and blog <a href="#id-22-cms-pages-and-blog" id="id-22-cms-pages-and-blog"></a>

`marketing.WebsitePage` and `marketing.BlogPost` are admin-authored records rendered on the public site, edited under `/console/website/`. The bundled content in `apps/marketing/content/` is the source of truth: the full website importer (`import_public_website`, run by `deploy_release`) treats bundled records as canonical and overwrites matching production pages and posts on each deploy, idempotent by slug. **Hand edits to bundled articles belong in source.**

`body_html` and `extra_head_html` are the **only** sanctioned `|safe` uses in the codebase, and only because they are admin-authored. Never render user-supplied content with `|safe`.

**Blog reading experience.** The index features the latest article on the unfiltered first page, then image cards, paginated in groups of 12; the featured article is one of those 12 and is not repeated. Search matches titles, excerpts and descriptions; topic selection intersects the search; both use a plain GET form and work without JavaScript. Pagination preserves search and topic; plain paginated pages have their own canonical URLs while filtered/search views are `noindex,follow`. Drafts and future publications stay out of results, topic choices, related posts, feeds and details.

`blog_presentation.article_with_outline` adds missing `h2` anchors to the *rendered copy* of trusted CMS HTML and builds the contents navigation on the server. Existing IDs and source markup are preserved, generated IDs avoid collisions, and **it never writes back to the database**.

`apps/marketing/content/seo_library.py` carries 100 hosting guides (guest operations/performance/storage/backups; DNS/networking/access/TLS/security; deployment/databases/containers/release workflows). Product boundaries stay explicit in that copy: customers administer their own guests and applications, LayerOne offers optional organization-wide VPS backup storage for snapshots and backups while still expecting independent off-server copies of irreplaceable data, and the networking articles do not promise IPv6, managed NAT or managed guest configuration.

`docs` is a **reserved first segment** so the website CMS cannot shadow the help center.

## 2.3 SEO, canonical URLs and analytics <a href="#id-23-seo-canonical-urls-and-analytics" id="id-23-seo-canonical-urls-and-analytics"></a>

**Canonical URLs and sitemap/robots intentionally hardcode `https://layeronecloud.com`** (`PUBLIC_SITE_BASE_URL` in `apps/core/views.py`); tests pin this. Do not "fix" it to be request-relative. Production `APP_BASE_URL` defaults to the same origin when unset or blank, and values whose hostname is the retired `pulsar67.com` resolve to it too, so a stale Railway value cannot survive a cutover.

**Analytics.** `AnalyticsConfig` (`apps/core/models.py`, singleton on `key="default"`) holds `is_enabled`, `google_tag_id`, `track_console_pages`. `is_active` means enabled *and* a tag present, so an enabled row with a blank ID renders nothing rather than a broken snippet.

`GOOGLE_TAG_ID_VALIDATOR` accepts `G-`/`GT-`/`AW-` plus 4–20 alphanumerics and **is a security boundary, not formatting**: the ID is interpolated into inline JavaScript and into the gtag.js URL, so a value containing a quote, slash or angle bracket would be stored XSS on every page. It lives on the model field, so the Django admin and any shell write are validated too. Widening it to accept free text, or adding `|safe`, turns the settings form into stored XSS.

`templates/includes/analytics.html` is included **exactly once**, from `layouts/_head_scripts.html` — the one partial `marketing.html`, `base.html` and `auth.html` all pull in. Including it per-layout double-counts every pageview. `track_console_pages` (default on) reuses `route_mode_for_request`, so unchecking it drops only admin-mode routes and keeps customer-facing pages that live under `/console/` (client ticketing) tracked — a plain path-prefix test would have silently broken those.

Deliberately not tracked: `403.html`, `404.html`, `500.html` (self-contained by design — inline CSS, no static files, no DB reads — so they render when the platform is broken; a 500 renders with no context processors at all), and email or CMS block previews. `active_analytics_config` swallows `OperationalError`/`ProgrammingError` so a pre-migration boot renders pages without a tag instead of 500ing.

## 2.4 Looking glass <a href="#id-24-looking-glass" id="id-24-looking-glass"></a>

The public network tool at `/looking-glass/` runs ping, traceroute and MTR from remote agents. **Runs are logged, including the rejected ones** (`LookingGlassRun`): rate-limited and no-agent-online attempts are recorded too, or the log cannot show a dead agent or a source hammering the tool.

* The requester address is stored as a **salted HMAC** keyed on `SECRET_KEY`, never in clear. The tool is rate limited per address, so the log has to tell two requests from one source apart; it does not need to name that source.
* `output_lines` is console-only and never rendered publicly. Only the per-region latency **aggregate** is ever published.
* Logging failures are swallowed and reported, so a full disk cannot turn a working network query into a 500.
* Ping min/avg/max and loss are parsed from the output. **MTR columns are read positionally after splitting off the host**, because a plain float scan over an MTR row picks digits out of the IP address (in `2.|-- 207.174.22.1 0.0% 5 12.4 …` the second float is `22.1`).
* Retention 30 days (`purge-old-looking-glass-runs`). Console page at `/console/monitoring/looking-glass/`, paginated at 50.

Execution is an **outbound polling queue**: the web app stores a short-lived job for the selected online agent, the agent claims it with its own bearer credential, streams bounded progress snapshots that also renew the claim lease, and posts the final result. The public page updates its command-style output panel in place. No public agent URL or inbound listener is required. A result from another agent is rejected without changing the run.

## 2.5 Public status pages <a href="#id-25-public-status-pages" id="id-25-public-status-pages"></a>

`status_pages.StatusPage` and `StatusPageComponent` hold the data; `apps.core` public views render it. A component may link to a `monitoring.Monitor`, and only visible components linked to a monitor are shown.

**Public status is incident driven, not a projection of `Monitor.current_status`.** A single failed check is not an outage: the thresholds decide when a monitor state change opens a monitor incident, and the incident is what the component reads. `apps/status_pages/selectors.py` (`_resolved_public_status`) resolves in this order:

| Condition                                                 | Public component status |
| --------------------------------------------------------- | ----------------------- |
| Monitor is `Paused`                                       | `Maintenance`           |
| Active monitor incident, severity `Major`/`Critical`      | `Major Outage`          |
| Active monitor incident, severity `Minor`/`Informational` | `Degraded`              |
| Monitor is `Unknown` (never checked, or flagged stale)    | `Unknown`               |
| No worker reported inside the stale-coverage window       | `Unknown`               |
| Otherwise                                                 | `Operational`           |

Two rows are deliberate and have burned us:

* **`Paused` outranks an open incident.** A paused monitor never runs again, so nothing can auto-resolve its incident; without this precedence, pausing a `Down` monitor for planned maintenance pinned the component to `Major Outage` permanently. `pause_monitor` also annotates any still-open incident so operators know monitoring evidence stopped on purpose.
* **No evidence is `Unknown`, never `Operational`.** A monitor never checked, or whose entire worker group has died, produces no results, so no failure counter increments and no incident can open.

An open incident **escalates in place**: a monitor sliding `Degraded` → `Down` raises the existing incident `Minor` → `Major`, so the component follows the service down instead of staying `Degraded`. Severity is only ever raised here; recovery resolves the incident.

**What the public layer receives:** component name, slug, group, resolved status, status source, last checked time, uptime windows and history, and per-region latency history (worker group name, region, map coordinates). **What it never receives:** monitor names, monitor targets, device names, hostnames, IPs, raw errors, metadata. Publishing where LayerOne measures from is a deliberate feature; publishing what is being measured is not.

Public status JSON lives at `/api/v1/public/status/` and `/api/v1/public/status/<site_slug>/`.

## 2.6 Promotional landing pages <a href="#id-26-promotional-landing-pages" id="id-26-promotional-landing-pages"></a>

Unlisted promo pages sell hidden `Plan` clones at a discount. They are `noindex, nofollow` and omitted from the sitemap because they are campaign prices rather than the canonical public catalog.

* `/let-promo/` — the LowEndTalk offer, currently generation 5 (see [4.15](/platform/billing/referrals-and-promotions.md#id-415-promotions)). Fourteen sizes: eight General Compute (`gc.nano`…`gc.4xlarge`) and six Network Optimized.
* `/promo/` — a generic shareable offer on the same 14 sizes, priced 10% below the current LET generation (monthly 43% off public, annual 52% off; `_percent_off` **floors**). No partner branding and no first-deposit match.
* `/builtbybit/` — the BuiltByBit offer, 30% below both public monthly and annual prices, on its own dedicated hidden plan rows.
* `/let-beta/` — the shared-hosting beta ([6.1](/platform/web-hosting/provisioning.md#id-61-catalog-and-let-beta)).

The promo pages read display prices **from the same rows checkout uses**, so a page cannot advertise a discount checkout does not honour, and they compute the displayed percentage against the matching public plan with a **floor** so the page can never overstate the saving.

**Retiring an offer** is one switch per page on `PromotionSettings`, edited at Console → Website → Promotions (`marketing:admin-promotions`, Super admin only — `marketing` is absent from `SUPPORT_CONSOLE_ROUTES`). A switched-off offer **keeps its URL** and renders `marketing/_promo_expired.html` ("This offer has expired", linking to `/pricing/`) rather than 404ing: these pages are linked from forum threads that stay readable for years, and the expired notice carries no prices because the rows behind a retired offer are no longer for sale.

The switches are **separate on purpose**. One `is_enabled` flag used to govern the `/let-promo/` page, `/let-beta/` reservations and the redeem code together, so pausing any one of them paused all three. They are now `let_vps_offer_enabled`, `let_beta_hosting_enabled` and `let_promo_code_enabled`. Two consequences follow from splitting them:

* `/let-promo/` reads `let_promo_code_enabled` **as well as** its own switch and drops every statement of the first-deposit match while the code is off, so the page cannot promise a bonus add-credit now refuses — the same rule [4.15](/platform/billing/referrals-and-promotions.md#id-415-promotions) applies to the match terms.
* `/let-beta/` checks an existing reservation **before** the switch, so closing the beta to new sign-ups does not hide a customer's own hosting account behind an expired notice. The switch closes the reserve path only.

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.layeronecloud.com/platform/public-site.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
