From a9a05aa105fc1fb8bf2e8fedfae4b94478de829c Mon Sep 17 00:00:00 2001 From: limitcool Date: Tue, 31 Mar 2026 00:25:58 +0800 Subject: [PATCH] perf: aggregate homepage data and trim frontend loading --- backend/src/controllers/site_settings.rs | 73 +++++++++++++++++++++++- frontend/src/layouts/BaseLayout.astro | 20 ++++++- frontend/src/lib/api/client.ts | 44 +++++++++++++- frontend/src/pages/index.astro | 18 ++---- 4 files changed, 137 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/site_settings.rs b/backend/src/controllers/site_settings.rs index 3f2abe4..5730f1a 100644 --- a/backend/src/controllers/site_settings.rs +++ b/backend/src/controllers/site_settings.rs @@ -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, + pub tags: Vec, + pub friend_links: Vec, + pub categories: Vec, +} + fn normalize_optional_string(value: Option) -> Option { 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) -> Result { + 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::>(); + + format::json(HomePageResponse { + site_settings, + posts, + tags, + friend_links, + categories, + }) +} + #[debug_handler] pub async fn show(State(ctx): State) -> Result { 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)) diff --git a/frontend/src/layouts/BaseLayout.astro b/frontend/src/layouts/BaseLayout.astro index 82b51c2..037da6e 100644 --- a/frontend/src/layouts/BaseLayout.astro +++ b/frontend/src/layouts/BaseLayout.astro @@ -279,10 +279,26 @@ const i18nPayload = JSON.stringify({ locale, messages }); })(); - + + - + +
diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 1a3ec82..3edca3d 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -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 { - 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('/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 { const categories = await this.fetch('/categories'); return categories.map(normalizeCategory); diff --git a/frontend/src/pages/index.astro b/frontend/src/pages/index.astro index 705a476..649dcb3 100644 --- a/frontend/src/pages/index.astro +++ b/frontend/src/pages/index.astro @@ -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() || '';