perf: aggregate homepage data and trim frontend loading

This commit is contained in:
2026-03-31 00:25:58 +08:00
parent 99b308e800
commit a9a05aa105
4 changed files with 137 additions and 18 deletions

View File

@@ -10,8 +10,10 @@ use uuid::Uuid;
use crate::{
controllers::admin::check_auth,
models::_entities::site_settings::{self, ActiveModel, Entity, Model},
services::ai,
models::_entities::{
categories, friend_links, posts, site_settings::{self, ActiveModel, Entity, Model}, tags,
},
services::{ai, content},
};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
@@ -154,6 +156,23 @@ pub struct PublicSiteSettingsResponse {
pub paragraph_comments_enabled: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct HomeCategorySummary {
pub id: i32,
pub name: String,
pub slug: String,
pub count: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct HomePageResponse {
pub site_settings: PublicSiteSettingsResponse,
pub posts: Vec<posts::Model>,
pub tags: Vec<tags::Model>,
pub friend_links: Vec<friend_links::Model>,
pub categories: Vec<HomeCategorySummary>,
}
fn normalize_optional_string(value: Option<String>) -> Option<String> {
value.and_then(|item| {
let trimmed = item.trim().to_string();
@@ -664,6 +683,55 @@ fn public_response(model: Model) -> PublicSiteSettingsResponse {
}
}
#[debug_handler]
pub async fn home(State(ctx): State<AppContext>) -> Result<Response> {
content::sync_markdown_posts(&ctx).await?;
let site_settings = public_response(load_current(&ctx).await?);
let posts = posts::Entity::find()
.order_by_desc(posts::Column::CreatedAt)
.all(&ctx.db)
.await?;
let tags = tags::Entity::find().all(&ctx.db).await?;
let friend_links = friend_links::Entity::find()
.order_by_desc(friend_links::Column::CreatedAt)
.all(&ctx.db)
.await?;
let category_items = categories::Entity::find()
.order_by_asc(categories::Column::Slug)
.all(&ctx.db)
.await?;
let categories = category_items
.into_iter()
.map(|category| {
let name = category
.name
.clone()
.unwrap_or_else(|| category.slug.clone());
let count = posts
.iter()
.filter(|post| post.category.as_deref().map(str::trim) == Some(name.as_str()))
.count();
HomeCategorySummary {
id: category.id,
name,
slug: category.slug,
count,
}
})
.collect::<Vec<_>>();
format::json(HomePageResponse {
site_settings,
posts,
tags,
friend_links,
categories,
})
}
#[debug_handler]
pub async fn show(State(ctx): State<AppContext>) -> Result<Response> {
format::json(public_response(load_current(&ctx).await?))
@@ -687,6 +755,7 @@ pub async fn update(
pub fn routes() -> Routes {
Routes::new()
.prefix("api/site_settings/")
.add("home", get(home))
.add("/", get(show))
.add("/", put(update))
.add("/", patch(update))

View File

@@ -279,10 +279,26 @@ const i18nPayload = JSON.stringify({ locale, messages });
})();
</script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
media="print"
onload="this.media='all'"
/>
<noscript>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
</noscript>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
media="print"
onload="this.media='all'"
>
<noscript>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
</noscript>
</head>
<body class="min-h-screen bg-[var(--bg)] text-[var(--text)] font-sans antialiased">
<div class="relative min-h-screen flex flex-col">

View File

@@ -132,6 +132,14 @@ export interface ApiSiteSettings {
paragraph_comments_enabled: boolean;
}
export interface ApiHomePagePayload {
site_settings: ApiSiteSettings;
posts: ApiPost[];
tags: ApiTag[];
friend_links: ApiFriendLink[];
categories: ApiCategory[];
}
export interface AiSource {
slug: string;
href: string;
@@ -398,8 +406,22 @@ class ApiClient {
}
async getPostBySlug(slug: string): Promise<UiPost | null> {
const posts = await this.getPosts();
return posts.find(post => post.slug === slug) || null;
const response = await fetch(`${this.baseUrl}/posts/slug/${encodeURIComponent(slug)}`, {
headers: {
'Content-Type': 'application/json',
},
});
if (response.status === 404) {
return null;
}
if (!response.ok) {
const errorText = await response.text().catch(() => '');
throw new Error(errorText || `API error: ${response.status} ${response.statusText}`);
}
return normalizePost((await response.json()) as ApiPost);
}
async getComments(
@@ -487,6 +509,24 @@ class ApiClient {
return normalizeSiteSettings(settings);
}
async getHomePageData(): Promise<{
siteSettings: SiteSettings;
posts: UiPost[];
tags: UiTag[];
friendLinks: AppFriendLink[];
categories: UiCategory[];
}> {
const payload = await this.fetch<ApiHomePagePayload>('/site_settings/home');
return {
siteSettings: normalizeSiteSettings(payload.site_settings),
posts: payload.posts.map(normalizePost),
tags: payload.tags.map(normalizeTag),
friendLinks: payload.friend_links.map(normalizeFriendLink),
categories: payload.categories.map(normalizeCategory),
};
}
async getCategories(): Promise<UiCategory[]> {
const categories = await this.fetch<ApiCategory[]>('/categories');
return categories.map(normalizeCategory);

View File

@@ -37,19 +37,13 @@ let apiError: string | null = null;
const { locale, t } = getI18n(Astro);
try {
const [settings, posts, rawTags, rawFriendLinks, nextCategories] = await Promise.all([
api.getSiteSettings(),
api.getPosts(),
api.getTags(),
api.getFriendLinks(),
api.getCategories(),
]);
const homeData = await api.getHomePageData();
siteSettings = settings;
allPosts = posts;
tags = rawTags.map(tag => tag.name);
friendLinks = rawFriendLinks.filter(friend => friend.status === 'approved');
categories = nextCategories;
siteSettings = homeData.siteSettings;
allPosts = homeData.posts;
tags = homeData.tags.map(tag => tag.name);
friendLinks = homeData.friendLinks.filter(friend => friend.status === 'approved');
categories = homeData.categories;
const filteredPosts = allPosts.filter(post => {
const normalizedCategory = post.category?.trim().toLowerCase() || '';