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