diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml
index 228275d..3339a31 100644
--- a/.github/workflows/pipeline.yaml
+++ b/.github/workflows/pipeline.yaml
@@ -7,12 +7,15 @@ on:
paths:
- ".github/workflows/pipeline.yaml"
- "Dockerfile"
+ - "deploy/single-node/**"
- "package.json"
- "yarn.lock"
- "scripts/github-actions/build-and-push-frontend-image.sh"
- "scripts/github-actions/compute-frontend-release-metadata.sh"
- "scripts/github-actions/render-frontend-build-args.sh"
+ - "scripts/github-actions/render-frontend-runtime-env.sh"
- "scripts/github-actions/prepare-frontend-build-context.sh"
+ - "scripts/github-actions/run-console-deploy-playbook.sh"
- "scripts/github-actions/verify-frontend-release.sh"
- "scripts/prebuild.sh"
- "contentlayer.config.ts"
@@ -42,7 +45,8 @@ concurrency:
cancel-in-progress: false
env:
- PRIMARY_DOMAIN: console.svc.plus
+ CANONICAL_DOMAIN: www.svc.plus
+ SERVED_DOMAINS: www.svc.plus,console.svc.plus
NEXT_PUBLIC_RUNTIME_ENVIRONMENT: prod
NEXT_PUBLIC_RUNTIME_REGION: cn
ACCOUNT_SERVICE_URL: https://accounts.svc.plus
@@ -176,4 +180,5 @@ jobs:
- name: Verify Frontend Release
run: |
bash scripts/github-actions/verify-frontend-release.sh \
- "${PRIMARY_DOMAIN}"
+ "${CANONICAL_DOMAIN}" \
+ "${SERVED_DOMAINS}"
diff --git a/deploy/single-node/.env.runtime.example b/deploy/single-node/.env.runtime.example
index 3758562..0f0f36c 100644
--- a/deploy/single-node/.env.runtime.example
+++ b/deploy/single-node/.env.runtime.example
@@ -1,21 +1,22 @@
# Compose settings
FRONTEND_IMAGE=ghcr.io/cloud-neutral-toolkit/dashboard:replace-me
-PRIMARY_DOMAIN=cn-console.svc.plus
+CANONICAL_DOMAIN=www.svc.plus
+SERVED_DOMAINS=www.svc.plus,console.svc.plus
# Frontend runtime
NODE_ENV=production
PORT=3000
RUNTIME_ENV=prod
REGION=cn
-APP_BASE_URL=https://cn-console.svc.plus
-NEXT_PUBLIC_APP_BASE_URL=https://cn-console.svc.plus
-NEXT_PUBLIC_SITE_URL=https://cn-console.svc.plus
-NEXT_PUBLIC_LOGIN_URL=https://cn-console.svc.plus/login
-NEXT_PUBLIC_DOCS_BASE_URL=https://cn-console.svc.plus/docs
+APP_BASE_URL=https://www.svc.plus
+NEXT_PUBLIC_APP_BASE_URL=https://www.svc.plus
+NEXT_PUBLIC_SITE_URL=https://www.svc.plus
+NEXT_PUBLIC_LOGIN_URL=https://www.svc.plus/login
+NEXT_PUBLIC_DOCS_BASE_URL=https://www.svc.plus/docs
SESSION_COOKIE_SECURE=true
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
-RUNTIME_HOSTNAME=cn-console.svc.plus
-DEPLOYMENT_HOSTNAME=cn-console.svc.plus
+RUNTIME_HOSTNAME=www.svc.plus
+DEPLOYMENT_HOSTNAME=www.svc.plus
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod
NEXT_PUBLIC_RUNTIME_REGION=cn
diff --git a/deploy/single-node/Caddyfile b/deploy/single-node/Caddyfile
index 78fcf10..8e9aea1 100644
--- a/deploy/single-node/Caddyfile
+++ b/deploy/single-node/Caddyfile
@@ -1,4 +1,4 @@
-{$PRIMARY_DOMAIN} {
+{$SERVED_DOMAINS} {
encode zstd gzip
handle_path /_next/static/* {
diff --git a/deploy/single-node/docker-compose.yml b/deploy/single-node/docker-compose.yml
index 45d47f2..d6e7be9 100644
--- a/deploy/single-node/docker-compose.yml
+++ b/deploy/single-node/docker-compose.yml
@@ -36,7 +36,7 @@ services:
- "80:80"
- "443:443"
environment:
- PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:?set PRIMARY_DOMAIN in .env.runtime}
+ SERVED_DOMAINS: ${SERVED_DOMAINS:?set SERVED_DOMAINS in .env.runtime}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- frontend_static:/srv:ro
diff --git a/docs/SEO-AUDIT-REPORT.md b/docs/SEO-AUDIT-REPORT.md
index 0c56956..9065b5d 100644
--- a/docs/SEO-AUDIT-REPORT.md
+++ b/docs/SEO-AUDIT-REPORT.md
@@ -214,7 +214,7 @@ export default function NotFound() {
import type { Metadata } from 'next'
export const metadata: Metadata = {
- metadataBase: new URL('https://console.svc.plus'),
+ metadataBase: new URL('https://www.svc.plus'),
title: {
default: 'Cloud-Neutral | Unified Cloud Native Tools',
template: '%s | Cloud-Neutral',
@@ -232,7 +232,7 @@ export const metadata: Metadata = {
openGraph: {
type: 'website',
locale: 'en_US',
- url: 'https://console.svc.plus',
+ url: 'https://www.svc.plus',
title: 'Cloud-Neutral | Unified Cloud Native Tools',
description: 'Unified tools for your cloud native stack',
siteName: 'Cloud-Neutral',
@@ -274,7 +274,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
+
{/* ... rest of head */}
{/* ... rest of layout */}
@@ -343,7 +343,7 @@ Disallow: /admin/
Disallow: /api/
Disallow: /internal/
-Sitemap: https://console.svc.plus/sitemap.xml
+Sitemap: https://www.svc.plus/sitemap.xml
```
---
@@ -360,8 +360,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Cloud-Neutral',
- url: 'https://console.svc.plus',
- logo: 'https://console.svc.plus/logo.png',
+ url: 'https://www.svc.plus',
+ logo: 'https://www.svc.plus/logo.png',
sameAs: [
'https://twitter.com/cloudneutral',
'https://github.com/x-evor',
@@ -372,10 +372,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Cloud-Neutral',
- url: 'https://console.svc.plus',
+ url: 'https://www.svc.plus',
potentialAction: {
'@type': 'SearchAction',
- target: 'https://console.svc.plus/search?q={search_term_string}',
+ target: 'https://www.svc.plus/search?q={search_term_string}',
'query-input': 'required name=search_term_string',
},
}
diff --git a/docs/en/deployment.md b/docs/en/deployment.md
index 9a5fafd..e2d7add 100644
--- a/docs/en/deployment.md
+++ b/docs/en/deployment.md
@@ -4,9 +4,11 @@
- Runtime: `Caddy + Docker Compose`
- Deploy host: `root@cn-console.svc.plus`
-- Domains:
- - `cn-console.svc.plus`
-- Frontend release workflow: `.github/workflows/service_release_frontend-deploy.yml`
+- Public domains:
+ - `www.svc.plus`
+ - `console.svc.plus`
+- Canonical public origin: `https://www.svc.plus`
+- Frontend release workflow: `.github/workflows/pipeline.yaml`
## Operating Model
@@ -20,7 +22,14 @@ The stack is static-first:
- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. Static assets and hashed CSS/JS files are extracted by the `frontend-assets` helper task, so the runtime no longer needs to compile anything on the single-node host.
- `docs.svc.plus` is the source of truth for rendered docs/blog pages; the browser does not call it directly.
-Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow builds/pushes the image, renders `.env.runtime` including `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` then updates Cloudflare DNS for the release domain (via `scripts/github-actions/update-release-dns.sh`) so `cn-console.svc.plus` points at the new environment.
+Releases are orchestrated through `.github/workflows/pipeline.yaml`. That workflow builds/pushes the image, renders `.env.runtime` including `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane DNS automation then updates Cloudflare DNS for the release domains (via `scripts/github-actions/update-release-dns.sh`) so both `www.svc.plus` and `console.svc.plus` resolve to the same environment.
+
+The release contract now uses:
+
+- `CANONICAL_DOMAIN=www.svc.plus`
+- `SERVED_DOMAINS=www.svc.plus,console.svc.plus`
+
+Validation must pass for both domains. A release is incomplete if either host serves a different runtime version, static asset family, or `dashboardUrl`.
This baseline is intentional for the weak-IO single-node host `root@cn-console.svc.plus`. No images are built on the target machine, keeping the deployment lightweight: the host only logs into GHCR, pulls the `dashboard` image, extracts assets into `frontend_static`, and starts `dashboard` plus `caddy` containers via `docker compose`.
diff --git a/docs/governance/release-process.md b/docs/governance/release-process.md
index bdd030d..8dc6945 100644
--- a/docs/governance/release-process.md
+++ b/docs/governance/release-process.md
@@ -1,6 +1,6 @@
# Release Process
-This page tracks release summaries for published versions of `console.svc.plus`.
+This page tracks release summaries for published versions of the public web console served under `www.svc.plus` and `console.svc.plus`.
## Current Release
@@ -49,3 +49,5 @@ Published commit: `0fab89e`
- GitHub Release: `https://github.com/x-evor/console.svc.plus/releases/tag/v0.2`
- Related docs: `docs/README.md`, `docs/en/README.md`, `docs/zh/README.md`
+- Release validation must verify both `www.svc.plus` and `console.svc.plus` against the same `releaseImageRef`, `releaseImageTag`, and `releaseCommit`.
+- `www.svc.plus` is the canonical public domain for metadata, sitemap, `dashboardUrl`, and shared links.
diff --git a/docs/integrations/cloudflare-web-analytics.md b/docs/integrations/cloudflare-web-analytics.md
index a8d612a..29057bd 100644
--- a/docs/integrations/cloudflare-web-analytics.md
+++ b/docs/integrations/cloudflare-web-analytics.md
@@ -48,7 +48,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=
### 本地开发
-写入 `console.svc.plus/.env.local`:
+写入当前前端仓库的 `.env.local`:
```bash
CLOUDFLARE_API_TOKEN=...
@@ -58,7 +58,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=...
### 线上部署
-把同名变量写入 `console.svc.plus` 的部署环境(例如 Vercel/Cloud Run 的环境变量配置)。
+把同名变量写入前端部署环境。
> 注意:这些变量属于服务端密钥,不要暴露到 `NEXT_PUBLIC_*`。
@@ -67,7 +67,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=...
部署后访问:
```bash
-curl -fsSL https://console.svc.plus/api/marketing/home-stats
+curl -fsSL https://www.svc.plus/api/marketing/home-stats
```
期望返回中 `visits.daily/weekly/monthly` 为数字(非 `null`)。
@@ -77,4 +77,3 @@ curl -fsSL https://console.svc.plus/api/marketing/home-stats
1. token 权限是否包含 Analytics Read
2. Account ID 是否与 siteTag 属于同一账号
3. 环境变量是否已在当前运行实例生效(重启/重新部署后再测)
-
diff --git a/docs/integrations/oidc-auth.md b/docs/integrations/oidc-auth.md
index 841658f..349edb6 100644
--- a/docs/integrations/oidc-auth.md
+++ b/docs/integrations/oidc-auth.md
@@ -6,7 +6,7 @@ This guide describes how to configure GitHub and Google OAuth login for the Clou
```
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
-│ Browser │ │ console.svc.plus │ │accounts.svc.plus │
+│ Browser │ │ www.svc.plus │ │accounts.svc.plus │
│ (User) │ │ (Frontend) │ │ (Backend) │
└──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘
│ 1. Click "Login │ │
@@ -40,7 +40,7 @@ This guide describes how to configure GitHub and Google OAuth login for the Clou
- A GitHub account with access to **Settings > Developer Settings**
- A Google account with access to [Google Cloud Console](https://console.cloud.google.com/)
-- Running `accounts.svc.plus` and `console.svc.plus` services
+- Running `accounts.svc.plus` and the frontend served under `www.svc.plus` / `console.svc.plus`
---
@@ -55,7 +55,7 @@ This guide describes how to configure GitHub and Google OAuth login for the Clou
| Field | Value |
|---|---|
| **Application name** | `Cloud Neutral Console` |
-| **Homepage URL** | `https://console.svc.plus` |
+| **Homepage URL** | `https://www.svc.plus` |
| **Authorization callback URL** | `https://accounts.svc.plus/api/auth/oauth/callback/github` |
| **Enable Device Flow** | ☐ (unchecked) |
@@ -119,7 +119,7 @@ No additional GitHub permissions are required.
|---|---|
| **Application type** | `Web application` |
| **Name** | `Cloud Neutral Console` |
-| **Authorized JavaScript origins** | `https://console.svc.plus` |
+| **Authorized JavaScript origins** | `https://www.svc.plus` |
| **Authorized redirect URIs** | `https://accounts.svc.plus/api/auth/oauth/callback/google` |
4. Click **"Create"**
@@ -150,7 +150,7 @@ GOOGLE_CLIENT_SECRET=
# ── General OAuth ──
OAUTH_REDIRECT_URL=https://accounts.svc.plus/api/auth/oauth/callback
-OAUTH_FRONTEND_URL=https://console.svc.plus
+OAUTH_FRONTEND_URL=https://www.svc.plus
```
These variables are referenced in `config/account.yaml`:
@@ -159,7 +159,7 @@ These variables are referenced in `config/account.yaml`:
auth:
oauth:
redirectUrl: "${OAUTH_REDIRECT_URL}"
- frontendUrl: "${OAUTH_FRONTEND_URL:-https://console.svc.plus}"
+ frontendUrl: "${OAUTH_FRONTEND_URL:-https://www.svc.plus}"
github:
clientId: "${GITHUB_CLIENT_ID}"
clientSecret: "${GITHUB_CLIENT_SECRET}"
@@ -172,7 +172,7 @@ auth:
---
-## 4. Frontend Configuration (console.svc.plus)
+## 4. Frontend Configuration (`www.svc.plus` canonical, `console.svc.plus` secondary)
The frontend resolves the accounts service URL **server-side** via `getAccountServiceBaseUrl()`, which reads:
@@ -209,7 +209,7 @@ If not set, the function falls back to a runtime default. **No `NEXT_PUBLIC_*` e
### OAuth login redirects to wrong domain
-Check that `OAUTH_FRONTEND_URL` in accounts.svc.plus matches the console domain where users should be redirected after authentication.
+Check that `OAUTH_FRONTEND_URL` in accounts.svc.plus matches the canonical public domain where users should be redirected after authentication. The current default is `https://www.svc.plus`.
### Google "Access blocked: This app's request is invalid"
diff --git a/docs/usage/deployment.md b/docs/usage/deployment.md
index f2f98d1..195118b 100644
--- a/docs/usage/deployment.md
+++ b/docs/usage/deployment.md
@@ -6,8 +6,9 @@
- Topology: `Caddy + Docker Compose + GitHub Actions`
- Deploy host: `root@cn-console.svc.plus`
- Public domains:
- - `https://cn-console.svc.plus`
-- Primary origin: `https://cn-console.svc.plus`
+ - `https://www.svc.plus`
+ - `https://console.svc.plus`
+- Canonical public origin: `https://www.svc.plus`
## Current Delivery Model
@@ -23,7 +24,7 @@ This is intentionally static-first for the current weak-IO single-node host. Dyn
## Control Plane & DNS Stage
-The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync`. Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/service_release_frontend-deploy.yml` finishes pushing the new image, the control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn-console.svc.plus`.
+The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync`. Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/pipeline.yaml` finishes pushing the new image, the control-plane DNS automation calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn-console.svc.plus`.
## Runtime Layout
@@ -52,7 +53,7 @@ Containers:
Workflow:
```text
-.github/workflows/service_release_frontend-deploy.yml
+.github/workflows/pipeline.yaml
```
Secrets required:
@@ -67,6 +68,8 @@ Secrets required:
Repository/environment variables recommended:
+- `CANONICAL_DOMAIN`
+- `SERVED_DOMAINS`
- `APP_BASE_URL`
- `NEXT_PUBLIC_APP_BASE_URL`
- `NEXT_PUBLIC_SITE_URL`
@@ -94,10 +97,10 @@ Repository/environment variables recommended:
2. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time.
3. The image is pushed to GHCR.
4. The workflow updates Cloudflare DNS for the release domain.
-5. The workflow renders `.env.runtime`, including docs service runtime endpoints and the `cn-console` origin settings.
+5. The workflow renders `.env.runtime`, including the canonical public origin and both served frontend domains.
6. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host.
7. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`.
-8. The workflow verifies `cn-console.svc.plus`.
+8. The workflow verifies both `www.svc.plus` and `console.svc.plus`, and fails the release if either domain serves a different runtime version.
## Verification Commands
@@ -113,7 +116,7 @@ rm -f deploy/single-node/.env.runtime
python3 - <<'PY'
from pathlib import Path
import yaml
-yaml.safe_load(Path('.github/workflows/service_release_frontend-deploy.yml').read_text())
+yaml.safe_load(Path('.github/workflows/pipeline.yaml').read_text())
print('workflow yaml ok')
PY
```
@@ -122,8 +125,10 @@ Remote checks:
```bash
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
-ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: cn-console.svc.plus' http://127.0.0.1/"
-curl -fsSIL https://cn-console.svc.plus
+ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: www.svc.plus' http://127.0.0.1/"
+ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: console.svc.plus' http://127.0.0.1/"
+curl -fsSIL https://www.svc.plus
+curl -fsSIL https://console.svc.plus
```
## Failure Signatures
@@ -132,7 +137,7 @@ curl -fsSIL https://cn-console.svc.plus
The workflow token or package visibility is wrong.
- `frontend-assets` fails
The image layout changed and no longer contains `/app/dashboard/static` or `/app/dashboard/public`.
-- `cn-console.svc.plus` returns `502`
+- `www.svc.plus` or `console.svc.plus` returns `502`
Caddy is up, but the `dashboard` container failed or is not reachable on port `3000`.
## Rollback
@@ -146,4 +151,4 @@ ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-f
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy"
```
-4. Verify `https://cn-console.svc.plus` again before closing the incident.
+4. Verify `https://www.svc.plus` and `https://console.svc.plus` again before closing the incident.
diff --git a/docs/zh/deployment.md b/docs/zh/deployment.md
index 70c42be..39e3305 100644
--- a/docs/zh/deployment.md
+++ b/docs/zh/deployment.md
@@ -5,8 +5,10 @@
- 运行拓扑: `Caddy + Docker Compose`
- 目标主机: `root@cn-console.svc.plus`
- 域名:
- - `cn-console.svc.plus`
-- 前端独立发布流水线: `.github/workflows/service_release_frontend-deploy.yml`
+ - `www.svc.plus`
+ - `console.svc.plus`
+- 公开首选域名: `www.svc.plus`
+- 前端独立发布流水线: `.github/workflows/pipeline.yaml`
## 运行方式
@@ -20,12 +22,19 @@
- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS)拷贝到 `frontend_static`。
- `docs.svc.plus` 是 docs/blog 的运行时内容源,浏览器不会直接访问它。
-发布由 `.github/workflows/service_release_frontend-deploy.yml` 驱动,CI 构建/推送镜像、渲染包含 `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL` 的 `.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面工作流 `.github/workflows/service_release_apiserver-deploy.yml` 通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS,使 `cn-console.svc.plus` 指向更新后的环境。
+发布由 `.github/workflows/pipeline.yaml` 驱动,CI 构建/推送镜像、渲染包含 `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL` 的 `.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面 DNS 自动化会通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS,使 `cn-console.svc.plus` 指向更新后的环境。
这是针对弱 IO 单机主机 `root@cn-console.svc.plus` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard` 与 `caddy`。
`docs.svc.plus` 已经是前端 docs/blog 内容的独立服务。
+当前发布合同要求:
+
+- `CANONICAL_DOMAIN=www.svc.plus`
+- `SERVED_DOMAINS=www.svc.plus,console.svc.plus`
+- 流水线必须同时校验两个域名的首页静态资源与 `/api/ping` 版本元数据完全一致
+- `dashboardUrl`、canonical、structured data 与 sitemap 默认统一输出 `https://www.svc.plus`
+
## 相关文档
- `usage/deployment.md`
diff --git a/docs/zh/governance/release-process.md b/docs/zh/governance/release-process.md
index eb812d6..d50219a 100644
--- a/docs/zh/governance/release-process.md
+++ b/docs/zh/governance/release-process.md
@@ -2,7 +2,7 @@
> English: `../../governance/release-process.md`
-本页用于记录 `console.svc.plus` 已发布版本的发布说明与变更摘要。
+本页用于记录公开控制台在 `www.svc.plus` 与 `console.svc.plus` 下发布版本的说明与变更摘要。
## 当前版本
@@ -50,4 +50,6 @@
## 备注
- GitHub Release:`https://github.com/x-evor/console.svc.plus/releases/tag/v0.2`
+- 发布校验必须同时验证 `www.svc.plus` 与 `console.svc.plus` 的 `releaseImageRef`、`releaseImageTag`、`releaseCommit` 完全一致。
+- `www.svc.plus` 是 metadata、sitemap、`dashboardUrl` 与公开分享链接的首选域名。
- 相关文档:`docs/README.md`、`docs/en/README.md`、`docs/zh/README.md`
diff --git a/scripts/github-actions/build-and-push-frontend-image.sh b/scripts/github-actions/build-and-push-frontend-image.sh
index c135598..926367c 100755
--- a/scripts/github-actions/build-and-push-frontend-image.sh
+++ b/scripts/github-actions/build-and-push-frontend-image.sh
@@ -14,7 +14,7 @@ require_env() {
}
require_env IMAGE_REF
-require_env PRIMARY_DOMAIN
+require_env CANONICAL_DOMAIN
BUILD_ARGS_FILE="$(mktemp)"
trap 'rm -f "${BUILD_ARGS_FILE}"' EXIT
diff --git a/scripts/github-actions/deploy-frontend-single-node.sh b/scripts/github-actions/deploy-frontend-single-node.sh
index 2e0c66b..5ef6609 100755
--- a/scripts/github-actions/deploy-frontend-single-node.sh
+++ b/scripts/github-actions/deploy-frontend-single-node.sh
@@ -21,7 +21,8 @@ require_env SINGLE_NODE_VPS_SSH_PRIVATE_KEY
require_env GHCR_USERNAME
require_env GHCR_PASSWORD
require_env FRONTEND_IMAGE
-require_env PRIMARY_DOMAIN
+require_env CANONICAL_DOMAIN
+require_env SERVED_DOMAINS
GHCR_REGISTRY="${GHCR_REGISTRY:-ghcr.io}"
diff --git a/scripts/github-actions/render-frontend-build-args.sh b/scripts/github-actions/render-frontend-build-args.sh
index b3c589d..549caf2 100755
--- a/scripts/github-actions/render-frontend-build-args.sh
+++ b/scripts/github-actions/render-frontend-build-args.sh
@@ -13,16 +13,16 @@ require_env() {
}
emit_lines() {
- require_env PRIMARY_DOMAIN
+ require_env CANONICAL_DOMAIN
- local primary_domain="${PRIMARY_DOMAIN}"
+ local canonical_domain="${CANONICAL_DOMAIN}"
printf 'NODE_BUILDER_IMAGE=%s\n' "${NODE_BUILDER_IMAGE:-node:22-bookworm}"
printf 'NODE_RUNTIME_IMAGE=%s\n' "${NODE_RUNTIME_IMAGE:-node:22-slim}"
printf 'CONTENTLAYER_BUILD=%s\n' "${CONTENTLAYER_BUILD:-true}"
- printf 'NEXT_PUBLIC_APP_BASE_URL=%s\n' "${NEXT_PUBLIC_APP_BASE_URL:-https://${primary_domain}}"
- printf 'NEXT_PUBLIC_SITE_URL=%s\n' "${NEXT_PUBLIC_SITE_URL:-https://${primary_domain}}"
- printf 'NEXT_PUBLIC_LOGIN_URL=%s\n' "${NEXT_PUBLIC_LOGIN_URL:-https://${primary_domain}/login}"
- printf 'NEXT_PUBLIC_DOCS_BASE_URL=%s\n' "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${primary_domain}/docs}"
+ printf 'NEXT_PUBLIC_APP_BASE_URL=%s\n' "${NEXT_PUBLIC_APP_BASE_URL:-https://${canonical_domain}}"
+ printf 'NEXT_PUBLIC_SITE_URL=%s\n' "${NEXT_PUBLIC_SITE_URL:-https://${canonical_domain}}"
+ printf 'NEXT_PUBLIC_LOGIN_URL=%s\n' "${NEXT_PUBLIC_LOGIN_URL:-https://${canonical_domain}/login}"
+ printf 'NEXT_PUBLIC_DOCS_BASE_URL=%s\n' "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${canonical_domain}/docs}"
printf 'NEXT_PUBLIC_RUNTIME_ENVIRONMENT=%s\n' "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}"
printf 'NEXT_PUBLIC_RUNTIME_REGION=%s\n' "${NEXT_PUBLIC_RUNTIME_REGION:-cn}"
printf 'NEXT_PUBLIC_GISCUS_REPO=%s\n' "${NEXT_PUBLIC_GISCUS_REPO:-x-evor/console.svc.plus}"
diff --git a/scripts/github-actions/render-frontend-runtime-env.sh b/scripts/github-actions/render-frontend-runtime-env.sh
index 0bb29df..103576b 100755
--- a/scripts/github-actions/render-frontend-runtime-env.sh
+++ b/scripts/github-actions/render-frontend-runtime-env.sh
@@ -22,25 +22,27 @@ require_env() {
}
require_env FRONTEND_IMAGE
-require_env PRIMARY_DOMAIN
+require_env CANONICAL_DOMAIN
+require_env SERVED_DOMAINS
append_env FRONTEND_IMAGE "${FRONTEND_IMAGE}"
-append_env PRIMARY_DOMAIN "${PRIMARY_DOMAIN}"
+append_env CANONICAL_DOMAIN "${CANONICAL_DOMAIN}"
+append_env SERVED_DOMAINS "${SERVED_DOMAINS}"
append_env NODE_ENV "production"
append_env PORT "${PORT:-3000}"
append_env RUNTIME_ENV "${RUNTIME_ENV:-prod}"
append_env REGION "${REGION:-cn}"
-append_env APP_BASE_URL "${APP_BASE_URL:-https://${PRIMARY_DOMAIN}}"
-append_env NEXT_PUBLIC_APP_BASE_URL "${NEXT_PUBLIC_APP_BASE_URL:-https://${PRIMARY_DOMAIN}}"
-append_env NEXT_PUBLIC_SITE_URL "${NEXT_PUBLIC_SITE_URL:-https://${PRIMARY_DOMAIN}}"
-append_env NEXT_PUBLIC_LOGIN_URL "${NEXT_PUBLIC_LOGIN_URL:-https://${PRIMARY_DOMAIN}/login}"
-append_env NEXT_PUBLIC_DOCS_BASE_URL "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${PRIMARY_DOMAIN}/docs}"
+append_env APP_BASE_URL "${APP_BASE_URL:-https://${CANONICAL_DOMAIN}}"
+append_env NEXT_PUBLIC_APP_BASE_URL "${NEXT_PUBLIC_APP_BASE_URL:-https://${CANONICAL_DOMAIN}}"
+append_env NEXT_PUBLIC_SITE_URL "${NEXT_PUBLIC_SITE_URL:-https://${CANONICAL_DOMAIN}}"
+append_env NEXT_PUBLIC_LOGIN_URL "${NEXT_PUBLIC_LOGIN_URL:-https://${CANONICAL_DOMAIN}/login}"
+append_env NEXT_PUBLIC_DOCS_BASE_URL "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${CANONICAL_DOMAIN}/docs}"
append_env SESSION_COOKIE_SECURE "${SESSION_COOKIE_SECURE:-true}"
append_env NEXT_PUBLIC_SESSION_COOKIE_SECURE "${NEXT_PUBLIC_SESSION_COOKIE_SECURE:-true}"
-append_env RUNTIME_HOSTNAME "${RUNTIME_HOSTNAME:-${PRIMARY_DOMAIN}}"
-append_env NEXT_RUNTIME_HOSTNAME "${NEXT_RUNTIME_HOSTNAME:-${PRIMARY_DOMAIN}}"
-append_env DEPLOYMENT_HOSTNAME "${DEPLOYMENT_HOSTNAME:-${PRIMARY_DOMAIN}}"
+append_env RUNTIME_HOSTNAME "${RUNTIME_HOSTNAME:-${CANONICAL_DOMAIN}}"
+append_env NEXT_RUNTIME_HOSTNAME "${NEXT_RUNTIME_HOSTNAME:-${CANONICAL_DOMAIN}}"
+append_env DEPLOYMENT_HOSTNAME "${DEPLOYMENT_HOSTNAME:-${CANONICAL_DOMAIN}}"
append_env NEXT_PUBLIC_RUNTIME_ENVIRONMENT "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}"
append_env NEXT_PUBLIC_RUNTIME_REGION "${NEXT_PUBLIC_RUNTIME_REGION:-cn}"
append_env ACCOUNT_SERVICE_URL "${ACCOUNT_SERVICE_URL:-https://accounts.svc.plus}"
diff --git a/scripts/github-actions/run-console-deploy-playbook.sh b/scripts/github-actions/run-console-deploy-playbook.sh
index 9947a4f..4eaa4b6 100644
--- a/scripts/github-actions/run-console-deploy-playbook.sh
+++ b/scripts/github-actions/run-console-deploy-playbook.sh
@@ -15,7 +15,8 @@ ansible_args=(
-e "GHCR_PASSWORD=${GHCR_PASSWORD:?GHCR_PASSWORD is required}"
-e "INTERNAL_SERVICE_TOKEN=${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}"
-e "ACCOUNT_SERVICE_URL=${ACCOUNT_SERVICE_URL:?ACCOUNT_SERVICE_URL is required}"
- -e "PRIMARY_DOMAIN=${PRIMARY_DOMAIN:?PRIMARY_DOMAIN is required}"
+ -e "CANONICAL_DOMAIN=${CANONICAL_DOMAIN:?CANONICAL_DOMAIN is required}"
+ -e "SERVED_DOMAINS=${SERVED_DOMAINS:?SERVED_DOMAINS is required}"
-e "NEXT_PUBLIC_RUNTIME_ENVIRONMENT=${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:?NEXT_PUBLIC_RUNTIME_ENVIRONMENT is required}"
-e "NEXT_PUBLIC_RUNTIME_REGION=${NEXT_PUBLIC_RUNTIME_REGION:?NEXT_PUBLIC_RUNTIME_REGION is required}"
-e "CLOUDFLARE_ZONE_TAG=${CLOUDFLARE_ZONE_TAG:?CLOUDFLARE_ZONE_TAG is required}"
diff --git a/scripts/github-actions/verify-frontend-release.sh b/scripts/github-actions/verify-frontend-release.sh
index 41bdda7..642b956 100755
--- a/scripts/github-actions/verify-frontend-release.sh
+++ b/scripts/github-actions/verify-frontend-release.sh
@@ -1,25 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
-PRIMARY_DOMAIN="${1:?usage: verify-frontend-release.sh }"
+CANONICAL_DOMAIN="${1:?usage: verify-frontend-release.sh }"
+SERVED_DOMAINS="${2:?usage: verify-frontend-release.sh }"
+EXPECTED_DASHBOARD_URL="https://${CANONICAL_DOMAIN}"
-primary_url="https://${PRIMARY_DOMAIN}"
+curl_headers=(
+ -H 'user-agent: Mozilla/5.0 (compatible; console-release-validator/1.0; +https://www.svc.plus)'
+ -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
+ -H 'accept-language: en-US,en;q=0.9'
+)
-curl -fsSIL "${primary_url}" >/dev/null
+trim() {
+ local value="$1"
+ value="${value#"${value%%[![:space:]]*}"}"
+ value="${value%"${value##*[![:space:]]}"}"
+ printf '%s' "${value}"
+}
-homepage_html="$(curl -fsSL "${primary_url}")"
-asset_path="$(printf '%s' "${homepage_html}" | grep -Eo '/_next/static/[^"'"'"' ]+\.(css|js)' | head -n 1)"
-if [[ -z "${asset_path}" ]]; then
- echo "Could not find a _next/static asset on the homepage" >&2
- exit 1
-fi
-
-curl -fsSIL "${primary_url}${asset_path}" >/dev/null
-printf 'verified static asset: %s%s\n' "${primary_url}" "${asset_path}"
-
-release_payload="$(curl -fsSL "${primary_url}/api/ping")"
-release_metadata="$(
- RELEASE_PAYLOAD="${release_payload}" python3 - <<'PY'
+parse_release_metadata() {
+ local payload="$1"
+ RELEASE_PAYLOAD="${payload}" python3 - <<'PY'
import json
import os
@@ -27,28 +28,141 @@ payload = json.loads(os.environ["RELEASE_PAYLOAD"])
print(payload.get("releaseImageRef", ""))
print(payload.get("releaseImageTag", ""))
print(payload.get("releaseCommit", ""))
+print(payload.get("dashboardUrl", ""))
PY
-)"
+}
-mapfile -t release_lines <<< "${release_metadata}"
-actual_image_ref="${release_lines[0]-}"
-actual_image_tag="${release_lines[1]-}"
-actual_release_commit="${release_lines[2]-}"
+require_http_200() {
+ local url="$1"
+ shift
-if [[ -z "${actual_image_ref}" || -z "${actual_image_tag}" || -z "${actual_release_commit}" ]]; then
- echo "Remote release metadata is incomplete: ${release_payload}" >&2
+ local http_code
+ http_code="$(curl -sS -o /dev/null -w '%{http_code}' "$@" "${url}")"
+ if [[ "${http_code}" != "200" ]]; then
+ echo "Expected HTTP 200 from ${url}, got ${http_code}" >&2
+ exit 1
+ fi
+}
+
+verify_domain() {
+ local domain="$1"
+ local url="https://${domain}"
+ local homepage_html asset_path release_payload release_metadata
+ local actual_image_ref actual_image_tag actual_release_commit actual_dashboard_url
+ local release_lines
+
+ require_http_200 "${url}" "${curl_headers[@]}"
+ printf 'verified homepage for %s: 200\n' "${domain}" >&2
+
+ homepage_html="$(curl -fsSL "${curl_headers[@]}" "${url}")"
+ asset_path="$(printf '%s' "${homepage_html}" | grep -Eo '/_next/static/[^"'"'"' ]+\.(css|js)' | head -n 1)"
+ if [[ -z "${asset_path}" ]]; then
+ echo "Could not find a _next/static asset on ${url}" >&2
+ exit 1
+ fi
+
+ require_http_200 "${url}${asset_path}" "${curl_headers[@]}"
+ printf 'verified static asset for %s: %s%s\n' "${domain}" "${url}" "${asset_path}" >&2
+
+ require_http_200 "${url}/api/ping" "${curl_headers[@]}"
+ release_payload="$(curl -fsSL "${curl_headers[@]}" "${url}/api/ping")"
+ release_metadata="$(parse_release_metadata "${release_payload}")"
+
+ mapfile -t release_lines <<< "${release_metadata}"
+ actual_image_ref="${release_lines[0]-}"
+ actual_image_tag="${release_lines[1]-}"
+ actual_release_commit="${release_lines[2]-}"
+ actual_dashboard_url="${release_lines[3]-}"
+
+ if [[ -z "${actual_image_ref}" || -z "${actual_image_tag}" || -z "${actual_release_commit}" ]]; then
+ echo "Remote release metadata is incomplete for ${domain}: ${release_payload}" >&2
+ exit 1
+ fi
+
+ if [[ ! "${actual_image_tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
+ echo "Remote image tag must contain a commit id for ${domain}, got: ${actual_image_tag}" >&2
+ exit 1
+ fi
+
+ if [[ "${actual_release_commit}" != "${actual_image_tag}" ]]; then
+ echo "Remote release commit mismatch for ${domain}: expected ${actual_image_tag}, got ${actual_release_commit}" >&2
+ exit 1
+ fi
+
+ if [[ "${actual_dashboard_url}" != "${EXPECTED_DASHBOARD_URL}" ]]; then
+ echo "Remote dashboardUrl mismatch for ${domain}: expected ${EXPECTED_DASHBOARD_URL}, got ${actual_dashboard_url}" >&2
+ exit 1
+ fi
+
+ printf 'verified release image for %s: %s\n' "${domain}" "${actual_image_ref}" >&2
+ printf 'verified release commit for %s: %s\n' "${domain}" "${actual_release_commit}" >&2
+ printf 'verified dashboardUrl for %s: %s\n' "${domain}" "${actual_dashboard_url}" >&2
+
+ printf '%s\t%s\t%s\t%s\t%s\n' "${domain}" "${actual_image_ref}" "${actual_image_tag}" "${actual_release_commit}" "${actual_dashboard_url}"
+}
+
+mapfile -t served_domains < <(
+ printf '%s' "${SERVED_DOMAINS}" | tr ',' '\n' | while IFS= read -r domain; do
+ domain="$(trim "${domain}")"
+ if [[ -n "${domain}" ]]; then
+ printf '%s\n' "${domain}"
+ fi
+ done
+)
+
+if [[ "${#served_domains[@]}" -eq 0 ]]; then
+ echo "No served domains were provided." >&2
exit 1
fi
-if [[ ! "${actual_image_tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
- echo "Remote image tag must contain a commit id, got: ${actual_image_tag}" >&2
+canonical_found=false
+declare -a verification_rows=()
+
+for domain in "${served_domains[@]}"; do
+ if [[ "${domain}" == "${CANONICAL_DOMAIN}" ]]; then
+ canonical_found=true
+ fi
+ verification_rows+=("$(verify_domain "${domain}")")
+done
+
+if [[ "${canonical_found}" != "true" ]]; then
+ echo "Canonical domain ${CANONICAL_DOMAIN} must be included in served domains: ${SERVED_DOMAINS}" >&2
exit 1
fi
-if [[ "${actual_release_commit}" != "${actual_image_tag}" ]]; then
- echo "Remote release commit mismatch: expected ${actual_image_tag}, got ${actual_release_commit}" >&2
- exit 1
-fi
+reference_image_ref=""
+reference_image_tag=""
+reference_release_commit=""
+reference_dashboard_url=""
-printf 'verified release image: %s\n' "${actual_image_ref}"
-printf 'verified release commit: %s\n' "${actual_release_commit}"
+for row in "${verification_rows[@]}"; do
+ IFS=$'\t' read -r domain actual_image_ref actual_image_tag actual_release_commit actual_dashboard_url <<< "${row}"
+
+ if [[ -z "${reference_image_ref}" ]]; then
+ reference_image_ref="${actual_image_ref}"
+ reference_image_tag="${actual_image_tag}"
+ reference_release_commit="${actual_release_commit}"
+ reference_dashboard_url="${actual_dashboard_url}"
+ continue
+ fi
+
+ if [[ "${actual_image_ref}" != "${reference_image_ref}" ]]; then
+ echo "Release image mismatch across served domains: ${domain} returned ${actual_image_ref}, expected ${reference_image_ref}" >&2
+ exit 1
+ fi
+
+ if [[ "${actual_image_tag}" != "${reference_image_tag}" ]]; then
+ echo "Release tag mismatch across served domains: ${domain} returned ${actual_image_tag}, expected ${reference_image_tag}" >&2
+ exit 1
+ fi
+
+ if [[ "${actual_release_commit}" != "${reference_release_commit}" ]]; then
+ echo "Release commit mismatch across served domains: ${domain} returned ${actual_release_commit}, expected ${reference_release_commit}" >&2
+ exit 1
+ fi
+
+ if [[ "${actual_dashboard_url}" != "${reference_dashboard_url}" ]]; then
+ echo "dashboardUrl mismatch across served domains: ${domain} returned ${actual_dashboard_url}, expected ${reference_dashboard_url}" >&2
+ exit 1
+ fi
+done
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 5934bf1..96da0b7 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -15,7 +15,7 @@ const DEFAULT_DESCRIPTION =
const DEFAULT_OG_IMAGE = '/icons/webchat.jpg'
export const metadata: Metadata = {
- metadataBase: new URL('https://console.svc.plus'),
+ metadataBase: new URL('https://www.svc.plus'),
title: {
default: DEFAULT_TITLE,
template: '%s | Cloud-Neutral',
@@ -93,8 +93,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Cloud-Neutral',
- url: 'https://console.svc.plus',
- logo: 'https://console.svc.plus/icons/cloudnative_32.png',
+ url: 'https://www.svc.plus',
+ logo: 'https://www.svc.plus/icons/cloudnative_32.png',
description: DEFAULT_DESCRIPTION,
}).replace(/ {
const config = loadRuntimeConfig({ hostname: "console.svc.plus" });
expect(config.authUrl).toBe("https://accounts.svc.plus");
- expect(config.dashboardUrl).toBe("https://console.svc.plus");
+ expect(config.dashboardUrl).toBe("https://www.svc.plus");
});
});