diff --git a/.env.example b/.env.example index 30c0226..7b72129 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index c56edc6..e38dae7 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -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 "" diff --git a/scripts/sync-blog-content.sh b/scripts/sync-blog-content.sh index 11b4888..aff8645 100755 --- a/scripts/sync-blog-content.sh +++ b/scripts/sync-blog-content.sh @@ -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." diff --git a/scripts/sync-doc-content.sh b/scripts/sync-doc-content.sh index 5fad5a6..8d63095 100755 --- a/scripts/sync-doc-content.sh +++ b/scripts/sync-doc-content.sh @@ -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." diff --git a/src/app/api/blogs/latest/route.ts b/src/app/api/blogs/latest/route.ts index 520f5c0..c8b7b0b 100644 --- a/src/app/api/blogs/latest/route.ts +++ b/src/app/api/blogs/latest/route.ts @@ -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, diff --git a/src/app/blogs/[...slug]/page.tsx b/src/app/blogs/[...slug]/page.tsx index 6bd645d..1037213 100644 --- a/src/app/blogs/[...slug]/page.tsx +++ b/src/app/blogs/[...slug]/page.tsx @@ -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) {
diff --git a/src/app/blogs/page.tsx b/src/app/blogs/page.tsx index e4feb7c..f5da971 100644 --- a/src/app/blogs/page.tsx +++ b/src/app/blogs/page.tsx @@ -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 ( diff --git a/src/app/docs/[collection]/[...slug]/page.tsx b/src/app/docs/[collection]/[...slug]/page.tsx index dabc723..e46f47d 100644 --- a/src/app/docs/[collection]/[...slug]/page.tsx +++ b/src/app/docs/[collection]/[...slug]/page.tsx @@ -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({
- +
diff --git a/src/app/docs/[collection]/page.tsx b/src/app/docs/[collection]/page.tsx index 560ca0f..3f10464 100644 --- a/src/app/docs/[collection]/page.tsx +++ b/src/app/docs/[collection]/page.tsx @@ -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, }: { diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index 25431dc..5b590a4 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -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() {
- +
); @@ -145,8 +136,7 @@ export default async function DocsHome() { No Documentation Found

- We could not find any documentation files. Please ensure content is - synced to src/content/doc. + We could not load the remote documentation service.

); diff --git a/src/app/docs/resources.server.ts b/src/app/docs/resources.server.ts index ae39178..664d778 100644 --- a/src/app/docs/resources.server.ts +++ b/src/app/docs/resources.server.ts @@ -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 => { export const getDocCollectionsForBuildTime = getDocCollections +export const getDocsHomeContent = cache(async () => { + if (!isDocsModuleEnabled()) { + return undefined + } + return getDocsHome() +}) + export async function getDocResources(): Promise { return getDocCollections() } @@ -31,22 +38,17 @@ export async function getDocResource(slug: string): Promise item.slug === targetSlug) - - if (!versionMatch) return undefined - return { collection, version: versionMatch } + try { + return await getDocPage(collectionSlug, targetSlug) + } catch { + return undefined + } } diff --git a/src/app/docs/types.ts b/src/app/docs/types.ts index bd7091f..a6694fa 100644 --- a/src/app/docs/types.ts +++ b/src/app/docs/types.ts @@ -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 } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index e48182f..e916ae0 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -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 { - const [posts, collections] = await Promise.all([getBlogPosts(), getDocCollections()]) + const [{ posts }, collections] = await Promise.all([ + getBlogList({ page: 1, pageSize: 500 }), + getDocCollections(), + ]) const staticEntries: MetadataRoute.Sitemap = [ { diff --git a/src/config/runtime-service-config.base.yaml b/src/config/runtime-service-config.base.yaml index 4658f97..0a3848a 100644 --- a/src/config/runtime-service-config.base.yaml +++ b/src/config/runtime-service-config.base.yaml @@ -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 diff --git a/src/config/runtime-service-config.prod.yaml b/src/config/runtime-service-config.prod.yaml index 21ea10b..ee8dc74 100644 --- a/src/config/runtime-service-config.prod.yaml +++ b/src/config/runtime-service-config.prod.yaml @@ -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 diff --git a/src/lib/docsServiceClient.ts b/src/lib/docsServiceClient.ts new file mode 100644 index 0000000..b886dbe --- /dev/null +++ b/src/lib/docsServiceClient.ts @@ -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(path: string): Promise { + 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 { + const lang = await detectLanguage(); + return request(`/api/v1/docs/home?lang=${lang}`); +} + +export async function getDocCollections(): Promise { + const lang = await detectLanguage(); + return request(`/api/v1/docs/collections?lang=${lang}`); +} + +export async function getDocPage( + collection: string, + slug: string, +): Promise { + const lang = await detectLanguage(); + return request( + `/api/v1/docs/pages/${collection}/${slug}?lang=${lang}`, + ); +} + +export async function getBlogList(params?: { + page?: number; + pageSize?: number; + category?: string; + query?: string; +}): Promise { + 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(`/api/v1/blogs?${search.toString()}`); +} + +export async function getBlogPost(slug: string): Promise { + const lang = await detectLanguage(); + return request(`/api/v1/blogs/${slug}?lang=${lang}`); +} + +export async function getLatestBlogPosts(limit = 7): Promise { + const lang = await detectLanguage(); + return request( + `/api/v1/home/latest-blogs?lang=${lang}&limit=${limit}`, + ); +} diff --git a/src/server/runtime-loader.ts b/src/server/runtime-loader.ts index fca22bf..3d7e076 100644 --- a/src/server/runtime-loader.ts +++ b/src/server/runtime-loader.ts @@ -53,6 +53,7 @@ export type RuntimeConfig = { apiBaseUrl?: string authUrl?: string dashboardUrl?: string + docsServiceUrl?: string internalApiBaseUrl?: string logLevel?: string [key: string]: unknown diff --git a/src/server/serviceConfig.ts b/src/server/serviceConfig.ts index 7e5d147..0a277ce 100644 --- a/src/server/serviceConfig.ts +++ b/src/server/serviceConfig.ts @@ -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