Skip to main content

yggdrasil/components/
share_meta.rs

1//! 分享元信息:`description`、Open Graph 与 Twitter 卡片。
2//!
3//! 只有服务端渲染的 HTML 会被社交平台读取,但客户端导航后挂载的标签也要与直达页面一致。
4//! 站点信息由中间件放进请求扩展([`crate::models::share::ShareContext`]),
5//! 再由 [`provide_share_context`](crate::components::share_meta::provide_share_context) 在 App 根部经 `SharedValue` 序列化,浏览器水合时读回。
6//! 标签顺序固定,以便水合按顺序对齐。
7//!
8//! 组件只写 `<head>`、没有可见内容,不进组件图鉴,故为 `pub(crate)`。
9
10use leptos::prelude::*;
11use leptos::text_prop::TextProp;
12use leptos_meta::Meta;
13
14use crate::hooks::ssr_resource::JsonSliceCodec;
15use crate::models::post::Post;
16use crate::models::share::{PostShareCard, ShareContext};
17
18/// 应用根组件调用一次:服务端从请求扩展读取分享上下文并序列化进页面,浏览器水合时读回同一份值。
19/// 必须在水合期间创建(App 根部):水合结束后才创建的 `SharedValue` 会在浏览器端重新求值,只能得到空值。
20pub(crate) fn provide_share_context() {
21    provide_context(
22        SharedValue::<ShareContext, JsonSliceCodec>::new_with_encoding(share_context_from_request)
23            .into_inner(),
24    );
25}
26
27fn share_context_from_request() -> ShareContext {
28    #[cfg(feature = "server")]
29    {
30        crate::request_ctx::request_extension::<ShareContext>().unwrap_or_default()
31    }
32    #[cfg(not(feature = "server"))]
33    {
34        ShareContext::default()
35    }
36}
37
38/// 读取分享上下文;没有上下文时(组件单测)返回空值。
39fn use_share_context() -> ShareContext {
40    use_context::<ShareContext>().unwrap_or_default()
41}
42
43/// 除文章页外的前台页面:站点描述与站点分享图。
44///
45/// 前台布局常驻,`path` 随导航变化,故为响应式(传当前显示页面的路径加查询串)。
46#[component]
47pub(crate) fn SiteShareMeta(#[prop(into)] path: Signal<String>) -> impl IntoView {
48    let ctx = use_share_context();
49    let base_url = ctx.base_url.clone();
50    view! {
51        <ShareTags
52            kind="website"
53            title="Yggdrasil"
54            description=ctx.description.clone()
55            url=move || format!("{base_url}{}", path.get())
56            image=format!("{}{}", ctx.base_url, ctx.site_image)
57            image_alt="Yggdrasil"
58        />
59    }
60}
61
62/// 文章页:标题、摘要、文章分享图与发布时间、标签。
63#[component]
64pub(crate) fn PostShareMeta(post: Post) -> impl IntoView {
65    let ctx = use_share_context();
66    let card = PostShareCard::from_post(&post);
67    let description = if card.summary.is_empty() {
68        ctx.description.clone()
69    } else {
70        card.summary.clone()
71    };
72    let published = post.published_at.unwrap_or(post.created_at).to_rfc3339();
73    let url = format!("{}/post/{}", ctx.base_url, post.slug);
74    let image = format!("{}{}", ctx.base_url, card.image_path(&post.slug));
75    let tags = post
76        .tags
77        .iter()
78        .map(|tag| view! { <Meta property="article:tag" content=tag.clone()/> })
79        .collect_view();
80
81    view! {
82        <ShareTags
83            kind="article"
84            title=card.title.clone()
85            description=description
86            url=url
87            image=image
88            image_alt=card.title.clone()
89        />
90        <Meta property="article:published_time" content=published/>
91        {tags}
92    }
93}
94
95/// 一组分享标签:description、Open Graph 与 Twitter 卡片(顺序固定,水合按顺序对齐)。
96#[component]
97fn ShareTags(
98    kind: &'static str,
99    #[prop(into)] title: TextProp,
100    #[prop(into)] description: TextProp,
101    #[prop(into)] url: TextProp,
102    #[prop(into)] image: TextProp,
103    #[prop(into)] image_alt: TextProp,
104) -> impl IntoView {
105    view! {
106        <Meta name="description" content=description.clone()/>
107        <Meta property="og:site_name" content="Yggdrasil"/>
108        <Meta property="og:locale" content="zh_CN"/>
109        <Meta property="og:type" content=kind/>
110        <Meta property="og:title" content=title/>
111        <Meta property="og:description" content=description/>
112        <Meta property="og:url" content=url/>
113        <Meta property="og:image" content=image/>
114        <Meta property="og:image:type" content="image/png"/>
115        <Meta property="og:image:width" content="1200"/>
116        <Meta property="og:image:height" content="630"/>
117        <Meta property="og:image:alt" content=image_alt/>
118        <Meta name="twitter:card" content="summary_large_image"/>
119    }
120}