perf: aggregate homepage data and trim frontend loading
This commit is contained in:
@@ -10,8 +10,10 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
controllers::admin::check_auth,
|
controllers::admin::check_auth,
|
||||||
models::_entities::site_settings::{self, ActiveModel, Entity, Model},
|
models::_entities::{
|
||||||
services::ai,
|
categories, friend_links, posts, site_settings::{self, ActiveModel, Entity, Model}, tags,
|
||||||
|
},
|
||||||
|
services::{ai, content},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
@@ -154,6 +156,23 @@ pub struct PublicSiteSettingsResponse {
|
|||||||
pub paragraph_comments_enabled: bool,
|
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> {
|
fn normalize_optional_string(value: Option<String>) -> Option<String> {
|
||||||
value.and_then(|item| {
|
value.and_then(|item| {
|
||||||
let trimmed = item.trim().to_string();
|
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]
|
#[debug_handler]
|
||||||
pub async fn show(State(ctx): State<AppContext>) -> Result<Response> {
|
pub async fn show(State(ctx): State<AppContext>) -> Result<Response> {
|
||||||
format::json(public_response(load_current(&ctx).await?))
|
format::json(public_response(load_current(&ctx).await?))
|
||||||
@@ -687,6 +755,7 @@ pub async fn update(
|
|||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
Routes::new()
|
Routes::new()
|
||||||
.prefix("api/site_settings/")
|
.prefix("api/site_settings/")
|
||||||
|
.add("home", get(home))
|
||||||
.add("/", get(show))
|
.add("/", get(show))
|
||||||
.add("/", put(update))
|
.add("/", put(update))
|
||||||
.add("/", patch(update))
|
.add("/", patch(update))
|
||||||
|
|||||||
@@ -279,10 +279,26 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
</head>
|
||||||
<body class="min-h-screen bg-[var(--bg)] text-[var(--text)] font-sans antialiased">
|
<body class="min-h-screen bg-[var(--bg)] text-[var(--text)] font-sans antialiased">
|
||||||
<div class="relative min-h-screen flex flex-col">
|
<div class="relative min-h-screen flex flex-col">
|
||||||
|
|||||||
@@ -132,6 +132,14 @@ export interface ApiSiteSettings {
|
|||||||
paragraph_comments_enabled: boolean;
|
paragraph_comments_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiHomePagePayload {
|
||||||
|
site_settings: ApiSiteSettings;
|
||||||
|
posts: ApiPost[];
|
||||||
|
tags: ApiTag[];
|
||||||
|
friend_links: ApiFriendLink[];
|
||||||
|
categories: ApiCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AiSource {
|
export interface AiSource {
|
||||||
slug: string;
|
slug: string;
|
||||||
href: string;
|
href: string;
|
||||||
@@ -398,8 +406,22 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPostBySlug(slug: string): Promise<UiPost | null> {
|
async getPostBySlug(slug: string): Promise<UiPost | null> {
|
||||||
const posts = await this.getPosts();
|
const response = await fetch(`${this.baseUrl}/posts/slug/${encodeURIComponent(slug)}`, {
|
||||||
return posts.find(post => post.slug === slug) || null;
|
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(
|
async getComments(
|
||||||
@@ -487,6 +509,24 @@ class ApiClient {
|
|||||||
return normalizeSiteSettings(settings);
|
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[]> {
|
async getCategories(): Promise<UiCategory[]> {
|
||||||
const categories = await this.fetch<ApiCategory[]>('/categories');
|
const categories = await this.fetch<ApiCategory[]>('/categories');
|
||||||
return categories.map(normalizeCategory);
|
return categories.map(normalizeCategory);
|
||||||
|
|||||||
@@ -37,19 +37,13 @@ let apiError: string | null = null;
|
|||||||
const { locale, t } = getI18n(Astro);
|
const { locale, t } = getI18n(Astro);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [settings, posts, rawTags, rawFriendLinks, nextCategories] = await Promise.all([
|
const homeData = await api.getHomePageData();
|
||||||
api.getSiteSettings(),
|
|
||||||
api.getPosts(),
|
|
||||||
api.getTags(),
|
|
||||||
api.getFriendLinks(),
|
|
||||||
api.getCategories(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
siteSettings = settings;
|
siteSettings = homeData.siteSettings;
|
||||||
allPosts = posts;
|
allPosts = homeData.posts;
|
||||||
tags = rawTags.map(tag => tag.name);
|
tags = homeData.tags.map(tag => tag.name);
|
||||||
friendLinks = rawFriendLinks.filter(friend => friend.status === 'approved');
|
friendLinks = homeData.friendLinks.filter(friend => friend.status === 'approved');
|
||||||
categories = nextCategories;
|
categories = homeData.categories;
|
||||||
|
|
||||||
const filteredPosts = allPosts.filter(post => {
|
const filteredPosts = allPosts.filter(post => {
|
||||||
const normalizedCategory = post.category?.trim().toLowerCase() || '';
|
const normalizedCategory = post.category?.trim().toLowerCase() || '';
|
||||||
|
|||||||
Reference in New Issue
Block a user