feat(docs): load docs and blogs from docs service

This commit is contained in:
Haitao Pan 2026-03-19 18:56:31 +08:00
parent c720083c2f
commit bf071a2679
18 changed files with 235 additions and 282 deletions

View File

@ -4,6 +4,8 @@ NEXT_PUBLIC_APP_BASE_URL=
NEXT_PUBLIC_SITE_URL=
NEXT_PUBLIC_LOGIN_URL=
NEXT_PUBLIC_DOCS_BASE_URL=
DOCS_SERVICE_URL=https://docs.svc.plus
DOCS_SERVICE_INTERNAL_URL=
SESSION_COOKIE_SECURE=true
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
RUNTIME_HOSTNAME=

View File

@ -13,22 +13,12 @@ echo "======================================"
# Step 1: Sync documentation from service repositories
echo ""
echo "[1/4] Syncing documentation content..."
bash scripts/sync-doc-content.sh
# Step 2: Sync blog content
echo ""
echo "[2/4] Syncing blog content..."
bash scripts/sync-blog-content.sh
# Step 3: Generate static content (homepage, products)
echo ""
echo "[3/4] Generating static content..."
echo "[1/2] Generating static content..."
npx tsx scripts/generate-content.ts
# Step 4: Build contentlayer
# Step 2: Build contentlayer
echo ""
echo "[4/4] Building contentlayer..."
echo "[2/2] Building contentlayer..."
node scripts/build-contentlayer.mjs
echo ""

View File

@ -1,34 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
CONTENT_DIR="src/content/blogs"
REPO_URL="https://github.com/cloud-neutral-workshop/knowledge.git"
# Ensure we're in the project root
cd "$(dirname "$0")/.."
if [ -d "${CONTENT_DIR}/.git" ]; then
echo "Updating existing git repo in ${CONTENT_DIR}..."
git -C "${CONTENT_DIR}" fetch --depth=1 origin main
git -C "${CONTENT_DIR}" reset --hard origin/main
git -C "${CONTENT_DIR}" clean -fdx
exit 0
fi
echo "Syncing content from ${REPO_URL} to ${CONTENT_DIR}..."
TMP_DIR=$(mktemp -d)
trap 'rm -rf "${TMP_DIR}"' EXIT
git clone --depth=1 "${REPO_URL}" "${TMP_DIR}/repo"
mkdir -p "${CONTENT_DIR}"
# Remove existing content but keep the directory
# Find and delete all files and directories inside CONTENT_DIR, but ignore errors if empty
find "${CONTENT_DIR}" -mindepth 1 -delete 2>/dev/null || true
# Copy content from repo to content dir, excluding .git
tar -C "${TMP_DIR}/repo" --exclude='.git' -cf - . | tar -C "${CONTENT_DIR}" -xf -
echo "Content synced successfully."
echo "blog content sync has moved to docs.svc.plus; script retained as a no-op."

View File

@ -1,163 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
# Sync documentation from multiple service repositories into the application
# This script pulls docs from each service repo and organizes them into src/content/doc/
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DOCS_DIR="${REPO_ROOT}/src/content/doc"
TMP_DIR=$(mktemp -d)
trap 'rm -rf "${TMP_DIR}"' EXIT
echo "==> Syncing service documentation to ${DOCS_DIR}"
# Define service repositories and their target directories
declare -A SERVICES=(
["https://github.com/cloud-neutral-toolkit/console.svc.plus.git"]="01-console"
["https://github.com/cloud-neutral-toolkit/accounts.svc.plus.git"]="02-accounts"
["https://github.com/cloud-neutral-toolkit/rag-server.svc.plus.git"]="03-rag-server"
["https://github.com/cloud-neutral-toolkit/postgresql.svc.plus.git"]="04-postgresql"
)
# Ensure docs directory exists
mkdir -p "${DOCS_DIR}"
# Clean up existing subdirectories (but keep index.md if we aren't regenerating it immediately, though we will)
# Actually, let's clean everything except .git and maybe custom files if any
find "${DOCS_DIR}" -mindepth 1 -maxdepth 1 -type d -not -name ".git" -exec rm -rf {} +
# Sync each service
for repo_url in "${!SERVICES[@]}"; do
target_dir="${SERVICES[$repo_url]}"
service_name=$(basename "${repo_url}" .git)
echo ""
echo "==> Processing ${service_name} -> docs/${target_dir}"
# Clone the repository
clone_dir="${TMP_DIR}/${service_name}"
echo " Cloning ${repo_url}..."
git clone --depth=1 --single-branch --branch main "${repo_url}" "${clone_dir}" 2>/dev/null || {
echo " Warning: Failed to clone ${repo_url}, skipping..."
continue
}
# Check if docs directory exists in the service repo
if [ ! -d "${clone_dir}/docs" ]; then
echo " Warning: No docs/ directory found in ${service_name}, skipping..."
continue
fi
# Create target directory
target_path="${DOCS_DIR}/${target_dir}"
mkdir -p "${target_path}"
# Copy documentation files
echo " Copying documentation files..."
cp -r "${clone_dir}/docs/"* "${target_path}/" 2>/dev/null || {
echo " Warning: Failed to copy docs from ${service_name}"
continue
}
# Add collection metadata if index.md exists
if [ -f "${target_path}/index.md" ]; then
# Check if frontmatter already has collection field
if ! grep -q "^collection:" "${target_path}/index.md"; then
# Add collection metadata to frontmatter
temp_file=$(mktemp)
awk -v collection="${target_dir}" '
BEGIN { in_frontmatter=0; added=0 }
/^---$/ {
in_frontmatter++
print
if (in_frontmatter == 1) {
print "collection: " collection
print "collectionLabel: " toupper(substr(collection, 4, 1)) substr(collection, 5)
added=1
}
next
}
{ print }
' "${target_path}/index.md" > "${temp_file}"
mv "${temp_file}" "${target_path}/index.md"
fi
fi
echo " ✓ Successfully synced ${service_name}"
done
echo ""
echo "==> Generating src/content/doc/index.md..."
# Generate the main index.md file
cat > "${DOCS_DIR}/index.md" << 'EOF'
---
title: Cloud-Neutral Toolkit Documentation
description: Comprehensive documentation for all Cloud-Neutral Toolkit services
---
# Cloud-Neutral Toolkit Documentation
Welcome to the **Cloud-Neutral Toolkit** documentation. This comprehensive guide covers all services in the toolkit, helping you build, deploy, and manage cloud-native applications across any vendor.
## 🚀 Services
EOF
# Add service sections dynamically
declare -A SERVICE_TITLES=(
["01-console"]="Console Service"
["02-accounts"]="Accounts & Identity Service"
["03-rag-server"]="RAG Server (AI/ML)"
["04-postgresql"]="PostgreSQL Service"
)
declare -A SERVICE_DESCRIPTIONS=(
["01-console"]="The main dashboard and control plane for managing your cloud-neutral infrastructure."
["02-accounts"]="Centralized authentication, authorization, and identity management with OIDC support."
["03-rag-server"]="Retrieval-Augmented Generation service for AI-powered features and intelligent assistance."
["04-postgresql"]="Managed PostgreSQL database service with cloud-neutral deployment options."
)
for target_dir in $(echo "${SERVICES[@]}" | tr ' ' '\n' | sort); do
if [ -d "${DOCS_DIR}/${target_dir}" ]; then
service_title="${SERVICE_TITLES[$target_dir]}"
service_desc="${SERVICE_DESCRIPTIONS[$target_dir]}"
cat >> "${DOCS_DIR}/index.md" << EOF
### ${service_title}
${service_desc}
**[View ${service_title} Documentation →](/docs/${target_dir}/index)**
EOF
fi
done
# Add footer
cat >> "${DOCS_DIR}/index.md" << 'EOF'
## 📚 Quick Links
- **[Getting Started](/docs/01-console/index)** - Begin with the Console Service
- **[Architecture Overview](/docs/01-console/architecture)** - Understand the system design
- **[API Reference](/docs/02-accounts/api)** - Explore the APIs
## 🔗 Resources
- [GitHub Organization](https://github.com/cloud-neutral-toolkit)
- [Community Forum](https://github.com/orgs/cloud-neutral-toolkit/discussions)
- [Issue Tracker](https://github.com/cloud-neutral-toolkit/console.svc.plus/issues)
---
*Last updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*
EOF
echo " ✓ Generated index.md"
echo ""
echo "==> Documentation sync complete!"
echo "docs content sync has moved to docs.svc.plus; script retained as a no-op."

View File

@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { getBlogPosts } from "@/lib/blogContent";
import { getLatestBlogPosts } from "@/lib/docsServiceClient";
export const dynamic = "force-dynamic";
@ -24,7 +24,7 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const limit = parseLimit(searchParams.get("limit"));
const posts = await getBlogPosts();
const posts = await getLatestBlogPosts(limit);
const latestPosts = posts.slice(0, limit).map((post) => ({
slug: post.slug,
title: post.title,

View File

@ -6,8 +6,7 @@ import { notFound } from "next/navigation";
import BrandCTA from "@components/BrandCTA";
import { PublicPageShell } from "@/components/public/PublicPageShell";
import { getBlogPostBySlug } from "@lib/blogContent";
import { renderMarkdownContent } from "@server/render-markdown";
import { getBlogPost } from "@lib/docsServiceClient";
type PageProps = {
params: Promise<{ slug: string | string[] }>;
@ -38,7 +37,12 @@ export async function generateMetadata({
const slugPath = Array.isArray(slugParam.slug)
? slugParam.slug.join("/")
: slugParam.slug;
const post = await getBlogPostBySlug(slugPath);
let post;
try {
post = await getBlogPost(slugPath);
} catch {
post = undefined;
}
if (!post) {
return { title: "Blog Post | Cloud-Neutral" };
@ -55,15 +59,19 @@ export default async function BlogPostPage({ params }: PageProps) {
const slugPath = Array.isArray(slugParam.slug)
? slugParam.slug.join("/")
: slugParam.slug;
const post = await getBlogPostBySlug(slugPath);
let post;
try {
post = await getBlogPost(slugPath);
} catch {
post = undefined;
}
if (!post) {
notFound();
}
const html = renderMarkdownContent(post.content);
const language: "zh" | "en" = /[\u4e00-\u9fff]/.test(
`${post.title} ${post.content}`,
`${post.title} ${post.excerpt} ${post.plaintext ?? ""}`,
)
? "zh"
: "en";
@ -123,7 +131,7 @@ export default async function BlogPostPage({ params }: PageProps) {
<section className="rounded-[2rem] border border-slate-900/10 bg-white/92 p-6 shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:p-8">
<article
className="public-doc-prose"
dangerouslySetInnerHTML={{ __html: html }}
dangerouslySetInnerHTML={{ __html: post.html }}
/>
</section>

View File

@ -1,4 +1,4 @@
export const dynamic = "error";
export const dynamic = "force-dynamic";
export const revalidate = false;
import type { Metadata } from "next";
@ -6,8 +6,8 @@ import { Suspense } from "react";
import BlogList from "@components/blog/BlogList";
import { PublicPageShell } from "@/components/public/PublicPageShell";
import type { BlogCategory, BlogPostSummary } from "@lib/blogContent";
import { getBlogCategories, getBlogPosts } from "@lib/blogContent";
import type { BlogCategoryPayload, BlogPostPayload } from "@lib/docsServiceClient";
import { getBlogList } from "@lib/docsServiceClient";
export const metadata: Metadata = {
title: "Blog | Cloud-Neutral",
@ -16,10 +16,10 @@ export const metadata: Metadata = {
};
export default async function BlogPage() {
const posts = await getBlogPosts();
const categories: BlogCategory[] = await getBlogCategories();
const postsWithoutContent: BlogPostSummary[] = posts.map(
({ content: _content, ...post }) => post,
const listing = await getBlogList({ page: 1, pageSize: 200 });
const categories: BlogCategoryPayload[] = listing.categories;
const postsWithoutContent = listing.posts.map(
({ html: _html, plaintext: _plaintext, sourcePath: _sourcePath, language: _language, ...post }: BlogPostPayload) => post,
);
return (

View File

@ -1,4 +1,4 @@
export const dynamic = "error";
export const dynamic = "force-dynamic";
export const revalidate = false;
import type { Metadata } from "next";
@ -6,7 +6,6 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { ChevronRight } from "lucide-react";
import DocArticle from "@/components/doc/DocArticle";
import DocMetaPanel from "@/components/doc/DocMetaPanel";
import { PublicPageIntro } from "@/components/public/PublicPageShell";
import { isFeatureEnabled } from "@lib/featureToggles";
@ -42,14 +41,6 @@ function DocsBreadcrumbs({
);
}
export const generateStaticParams = async () => {
if (!isFeatureEnabled("appModules", "/docs")) {
return [];
}
return getDocVersionParams();
};
export const dynamicParams = false;
export async function generateMetadata({
@ -109,7 +100,10 @@ export default async function DocVersionPage({
</section>
<section className="rounded-[1rem] border border-slate-900/8 bg-white/90 p-5 shadow-[var(--shadow-soft)] lg:p-6">
<DocArticle content={version.content} />
<article
className="public-doc-prose"
dangerouslySetInnerHTML={{ __html: version.html }}
/>
</section>
<Feedback />

View File

@ -1,4 +1,4 @@
export const dynamic = 'error'
export const dynamic = 'force-dynamic'
import { notFound, redirect } from 'next/navigation'
@ -7,16 +7,6 @@ import { isFeatureEnabled } from '@lib/featureToggles'
export const dynamicParams = false
export const generateStaticParams = async () => {
if (!isFeatureEnabled('appModules', '/docs')) {
return []
}
// 构建时优先使用本地 fallback 数据避免外部API调用
const collections = await getDocCollectionsForBuildTime()
return collections.map((doc) => ({ collection: doc.slug }))
}
export default async function CollectionPage({
params,
}: {

View File

@ -1,29 +1,17 @@
import { promises as fs } from "fs";
import path from "path";
export const dynamic = "force-dynamic";
import matter from "gray-matter";
import { ArrowRight, BookCopy, Files } from "lucide-react";
import Link from "next/link";
import DocArticle from "@/components/doc/DocArticle";
import { PublicPageIntro } from "@/components/public/PublicPageShell";
import { getDocCollections } from "./resources.server";
import { getDocCollections, getDocsHomeContent } from "./resources.server";
export default async function DocsHome() {
try {
const indexPath = path.join(
process.cwd(),
"src",
"content",
"doc",
"index.md",
);
const [fileContent, collections] = await Promise.all([
fs.readFile(indexPath, "utf-8"),
const [home, collections] = await Promise.all([
getDocsHomeContent(),
getDocCollections(),
]);
const { data: frontmatter, content } = matter(fileContent);
const articleCount = collections.reduce(
(sum, collection) => sum + collection.versions.length,
0,
@ -35,9 +23,9 @@ export default async function DocsHome() {
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem] lg:items-end">
<PublicPageIntro
eyebrow="Documentation"
title={frontmatter.title || "Documentation"}
title={home?.title || "Documentation"}
subtitle={
frontmatter.description ||
home?.description ||
"Unified references for Cloud-Neutral Toolkit services."
}
titleClassName="editorial-display text-[2.8rem] tracking-[-0.06em] sm:text-[3.4rem]"
@ -132,7 +120,10 @@ export default async function DocsHome() {
Overview
</p>
</div>
<DocArticle content={content} />
<article
className="public-doc-prose"
dangerouslySetInnerHTML={{ __html: home?.html ?? "" }}
/>
</section>
</div>
);
@ -145,8 +136,7 @@ export default async function DocsHome() {
No Documentation Found
</h3>
<p className="mx-auto mt-3 max-w-xl text-sm leading-6 text-text-muted">
We could not find any documentation files. Please ensure content is
synced to <code>src/content/doc</code>.
We could not load the remote documentation service.
</p>
</div>
);

View File

@ -2,7 +2,7 @@ import 'server-only'
import { cache } from 'react'
import { getDocCollection, getDocCollections as loadDocCollections, getDocParams } from '@lib/docContent'
import { getDocCollections as loadDocCollections, getDocPage, getDocsHome } from '@lib/docsServiceClient'
import { isFeatureEnabled } from '@lib/featureToggles'
import type { DocCollection } from './types'
@ -17,6 +17,13 @@ export const getDocCollections = cache(async (): Promise<DocCollection[]> => {
export const getDocCollectionsForBuildTime = getDocCollections
export const getDocsHomeContent = cache(async () => {
if (!isDocsModuleEnabled()) {
return undefined
}
return getDocsHome()
})
export async function getDocResources(): Promise<DocCollection[]> {
return getDocCollections()
}
@ -31,22 +38,17 @@ export async function getDocResource(slug: string): Promise<DocCollection | unde
}
export async function getDocVersionParams() {
if (!isDocsModuleEnabled()) {
return []
}
return getDocParams()
return []
}
export async function getDocVersion(collectionSlug: string, slugSegments: string | string[]) {
if (!isDocsModuleEnabled()) {
return undefined
}
const collection = await getDocCollection(collectionSlug)
if (!collection) return undefined
const targetSlug = Array.isArray(slugSegments) ? slugSegments.join('/') : slugSegments
const versionMatch = collection.versions.find((item) => item.slug === targetSlug)
if (!versionMatch) return undefined
return { collection, version: versionMatch }
try {
return await getDocPage(collectionSlug, targetSlug)
} catch {
return undefined
}
}

View File

@ -5,8 +5,10 @@ export interface DocVersionOption {
description: string
updatedAt?: string
tags?: string[]
content: string
isMdx: boolean
content?: string
html: string
toc?: Array<{ level: number; title: string; anchor: string }>
isMdx?: boolean
category?: string
subcategory?: boolean
}

View File

@ -1,7 +1,6 @@
import type { MetadataRoute } from 'next'
import { getBlogPosts } from '@/lib/blogContent'
import { getDocCollections } from '@/lib/docContent'
import { getBlogList, getDocCollections } from '@/lib/docsServiceClient'
import { PRODUCT_LIST } from '@/modules/products/registry'
const baseUrl = 'https://console.svc.plus'
@ -10,7 +9,10 @@ export const dynamic = 'force-dynamic'
export const revalidate = 3600 // Revalidate every hour
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const [posts, collections] = await Promise.all([getBlogPosts(), getDocCollections()])
const [{ posts }, collections] = await Promise.all([
getBlogList({ page: 1, pageSize: 500 }),
getDocCollections(),
])
const staticEntries: MetadataRoute.Sitemap = [
{

View File

@ -2,4 +2,5 @@
apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app
authUrl: https://accounts-svc-plus-266500572462.asia-northeast1.run.app
dashboardUrl: https://console.svc.plus
docsServiceUrl: https://docs.svc.plus
logLevel: info

View File

@ -5,7 +5,9 @@ regions:
apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app
authUrl: https://accounts-svc-plus-266500572462.asia-northeast1.run.app
dashboardUrl: https://console.svc.plus
docsServiceUrl: https://docs.svc.plus
global:
apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app
authUrl: https://accounts-svc-plus-266500572462.asia-northeast1.run.app
dashboardUrl: https://console.svc.plus
docsServiceUrl: https://docs.svc.plus

View File

@ -0,0 +1,139 @@
import "server-only";
import { headers } from "next/headers";
import { buildInternalServiceHeaders } from "@/server/internalServiceAuth";
import { getDocsServiceBaseUrl } from "@server/serviceConfig";
export type DocsHomePayload = {
title: string;
description: string;
html: string;
};
export type DocVersionPayload = {
slug: string;
label: string;
title: string;
description: string;
updatedAt?: string;
tags: string[];
html: string;
toc: Array<{ level: number; title: string; anchor: string }>;
category?: string;
};
export type DocCollectionPayload = {
slug: string;
title: string;
description: string;
updatedAt?: string;
tags: string[];
versions: DocVersionPayload[];
defaultVersionSlug: string;
category?: string;
};
export type DocPagePayload = {
collection: DocCollectionPayload;
version: DocVersionPayload;
breadcrumbs: Array<{ label: string; href: string }>;
};
export type BlogCategoryPayload = {
key: string;
label: string;
};
export type BlogPostPayload = {
slug: string;
title: string;
author?: string;
date?: string;
tags: string[];
excerpt: string;
html: string;
category?: BlogCategoryPayload;
language?: string;
sourcePath: string;
plaintext?: string;
};
export type BlogListPayload = {
posts: BlogPostPayload[];
categories: BlogCategoryPayload[];
page: number;
pageSize: number;
total: number;
totalPages: number;
};
async function detectLanguage(): Promise<"zh" | "en"> {
const store = await headers();
const preferred = store.get("x-language") ?? store.get("accept-language") ?? "";
return preferred.toLowerCase().includes("zh") ? "zh" : "en";
}
async function request<T>(path: string): Promise<T> {
const baseUrl = getDocsServiceBaseUrl();
const response = await fetch(`${baseUrl}${path}`, {
cache: "no-store",
headers: buildInternalServiceHeaders({
Accept: "application/json",
}),
});
if (!response.ok) {
throw new Error(`docs service request failed: ${response.status} ${path}`);
}
return (await response.json()) as T;
}
export async function getDocsHome(): Promise<DocsHomePayload> {
const lang = await detectLanguage();
return request<DocsHomePayload>(`/api/v1/docs/home?lang=${lang}`);
}
export async function getDocCollections(): Promise<DocCollectionPayload[]> {
const lang = await detectLanguage();
return request<DocCollectionPayload[]>(`/api/v1/docs/collections?lang=${lang}`);
}
export async function getDocPage(
collection: string,
slug: string,
): Promise<DocPagePayload> {
const lang = await detectLanguage();
return request<DocPagePayload>(
`/api/v1/docs/pages/${collection}/${slug}?lang=${lang}`,
);
}
export async function getBlogList(params?: {
page?: number;
pageSize?: number;
category?: string;
query?: string;
}): Promise<BlogListPayload> {
const lang = await detectLanguage();
const search = new URLSearchParams();
search.set("lang", lang);
search.set("page", String(params?.page ?? 1));
search.set("pageSize", String(params?.pageSize ?? 10));
if (params?.category) search.set("category", params.category);
if (params?.query) search.set("query", params.query);
return request<BlogListPayload>(`/api/v1/blogs?${search.toString()}`);
}
export async function getBlogPost(slug: string): Promise<BlogPostPayload> {
const lang = await detectLanguage();
return request<BlogPostPayload>(`/api/v1/blogs/${slug}?lang=${lang}`);
}
export async function getLatestBlogPosts(limit = 7): Promise<BlogPostPayload[]> {
const lang = await detectLanguage();
return request<BlogPostPayload[]>(
`/api/v1/home/latest-blogs?lang=${lang}&limit=${limit}`,
);
}

View File

@ -53,6 +53,7 @@ export type RuntimeConfig = {
apiBaseUrl?: string
authUrl?: string
dashboardUrl?: string
docsServiceUrl?: string
internalApiBaseUrl?: string
logLevel?: string
[key: string]: unknown

View File

@ -4,6 +4,7 @@ import { loadRuntimeConfig } from './runtime-loader'
const FALLBACK_ACCOUNT_SERVICE_URL = 'https://accounts.svc.plus'
const FALLBACK_SERVER_SERVICE_URL = 'https://api.svc.plus'
const FALLBACK_DOCS_SERVICE_URL = 'https://docs.svc.plus'
const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]'])
@ -19,6 +20,12 @@ function getRuntimeDefaultServerServiceUrl(): string {
return candidate ?? FALLBACK_SERVER_SERVICE_URL
}
function getRuntimeDefaultDocsServiceUrl(): string {
const runtime = loadRuntimeConfig()
const candidate = typeof runtime.docsServiceUrl === 'string' ? runtime.docsServiceUrl : undefined
return candidate ?? FALLBACK_DOCS_SERVICE_URL
}
function readEnvValue(...keys: string[]): string | undefined {
@ -108,6 +115,12 @@ const SERVER_INTERNAL_URL_ENV_KEYS = [
'INTERNAL_SERVER_SERVICE_URL',
] as const
const DOCS_SERVICE_URL_ENV_KEYS = [
'DOCS_SERVICE_URL',
'NEXT_PUBLIC_DOCS_SERVICE_URL',
'DOCS_SERVICE_INTERNAL_URL',
] as const
export function getInternalServerServiceBaseUrl(): string {
const configured = readEnvValue(...SERVER_INTERNAL_URL_ENV_KEYS)
if (configured) {
@ -118,6 +131,11 @@ export function getInternalServerServiceBaseUrl(): string {
return getServerServiceBaseUrl()
}
export function getDocsServiceBaseUrl(): string {
const configured = readEnvValue(...DOCS_SERVICE_URL_ENV_KEYS)
return normalizeBaseUrl(configured ?? getRuntimeDefaultDocsServiceUrl())
}
export const serviceConfig = {
account: {
baseUrl: getAccountServiceBaseUrl(),
@ -125,4 +143,7 @@ export const serviceConfig = {
server: {
baseUrl: getServerServiceBaseUrl(),
},
docs: {
baseUrl: getDocsServiceBaseUrl(),
},
} as const