feat(docs): load docs and blogs from docs service
This commit is contained in:
parent
c720083c2f
commit
bf071a2679
@ -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=
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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,
|
||||
}: {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 = [
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
139
src/lib/docsServiceClient.ts
Normal file
139
src/lib/docsServiceClient.ts
Normal 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}`,
|
||||
);
|
||||
}
|
||||
@ -53,6 +53,7 @@ export type RuntimeConfig = {
|
||||
apiBaseUrl?: string
|
||||
authUrl?: string
|
||||
dashboardUrl?: string
|
||||
docsServiceUrl?: string
|
||||
internalApiBaseUrl?: string
|
||||
logLevel?: string
|
||||
[key: string]: unknown
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user