footer-link-columns
link / separator / icon / heading / text を合成した、定番のリンクカラム型 footer です。上段はブランド列(ロゴマーク・名称・説明)とカテゴリ見出し付きのリンク列 2〜5 群、下段は区切り線を挟んだ著作権表示(インスタンスにより SNS アイコンリンクを添える)で構成します。法務リンク(Privacy Policy・Terms of Service)は実在ページが無いため掲載しません。
狭い幅ではブランド列の下にリンク列が 2 列 grid で並び、lg(64rem)以上で全列が横 1 行に並びます。<form> を持たず状態を持たない静的な表示です。
Demo
使用部品
Rust コード
Rust
use fandhe_frontend_core::{div, el, footer, li, text, ul, Node};
use fandhe_frontend_pre_styled_ui::heading::{self, HeadingLevel, HeadingProps};
use fandhe_frontend_pre_styled_ui::icon::{icon, IconProps};
use fandhe_frontend_pre_styled_ui::link::{self, LinkProps};
use fandhe_frontend_pre_styled_ui::separator::{self, SeparatorProps};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};
use fandhe_frontend_pre_styled_ui::Size;
/// リンク先の固定外部 URL(モジュール doc「リンク先の方針」節参照)。
/// 表示名と遷移先の対応が付く実在ページのみを使い、対応が付かない項目
/// (SNS アカウント・Privacy Policy・Terms of Service 等)は非表示にする
/// (Bugbot 指摘 PR #3271)。
const REPO: &str = "https://github.com/Fandhe-AI/fandhe-frontend";
/// docs サイト「Guide」の実在ページ(`site/nav.toml` `index_path = "/guides/"`)。
const GUIDE_URL: &str = "https://fandhe-ai.github.io/fandhe-frontend/guides/";
/// docs サイト「API Reference」の実在ページ(同 `index_path = "/api/"`)。
const API_REFERENCE_URL: &str = "https://fandhe-ai.github.io/fandhe-frontend/api/";
/// docs サイト「Examples」の実在ページ(同 `index_path = "/examples/"`)。
const EXAMPLES_URL: &str = "https://fandhe-ai.github.io/fandhe-frontend/examples/";
/// リポジトリのリリース一覧(「Changelog」の実在先)。
const RELEASES_URL: &str = "https://github.com/Fandhe-AI/fandhe-frontend/releases";
/// リポジトリの Discussions(「Discussions」の実在先)。
const DISCUSSIONS_URL: &str = "https://github.com/Fandhe-AI/fandhe-frontend/discussions";
/// docs サイトのトップページ(フレームワーク概要を紹介する `site/index.md`
/// が原稿。「About」の実在先。codex-review 指摘 PR #3271: 従来は
/// [`REPO`](リポジトリのトップページ)を充てていたが、表示名「About」が
/// 指す「このフレームワークの説明」に対応するのは docs サイト側の概要
/// ページであり、こちらへ差し替える)。
const HOME_URL: &str = "https://fandhe-ai.github.io/fandhe-frontend/";
/// 自作の幾何パスによる装飾アイコン(`feature_three_column_icons::
/// geo_icon` と同型の線画。`label` は呼び出し側が指定する)。
fn geo_icon(path_d: &'static str, label: Option<&'static str>) -> Node {
icon(
&IconProps {
size: Size::Md,
label,
..IconProps::default()
},
vec![("data-blocks-footer-link-columns-icon", "")],
vec![el(
"path",
vec![
("d", path_d),
("fill", "none"),
("stroke", "currentColor"),
("stroke-width", "2"),
("stroke-linecap", "round"),
("stroke-linejoin", "round"),
],
vec![],
)],
)
}
/// ロゴマーク(装飾扱い、`label: None`)。
fn logo_mark() -> Node {
geo_icon(
"M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5",
None,
)
}
/// SNS アイコンのみのリンク(accessible name はリンクごと異なる固定
/// 文字列、WCAG 2.4.4)。`href` は呼び出し側が実在の遷移先を指定する
/// (モジュール doc「リンク先の方針」節参照)。
fn social_icon_link(href: &'static str, path_d: &'static str, label: &'static str) -> Node {
link::root(
href,
&LinkProps {
external: true,
..LinkProps::default()
},
vec![("data-blocks-footer-link-columns-social-link", "")],
vec![geo_icon(path_d, Some(label))],
)
}
/// リンク群 1 個(見出し + `ul`/`li` のリンク一覧)。各項目は
/// `(href, label)` の組で、実在する遷移先のみを渡す契約(モジュール doc
/// 「リンク先の方針」節参照)。
fn link_group(heading_text: &'static str, links: &[(&'static str, &'static str)]) -> Node {
let items: Vec<Node> = links
.iter()
.map(|(href, label)| {
li(
vec![],
vec![link::root(
href,
&LinkProps::default(),
vec![],
vec![text(*label)],
)],
)
})
.collect();
div(
vec![("class", "blocks-footer-link-columns-group")],
vec![
heading::heading(
HeadingLevel::H4,
&HeadingProps::default(),
vec![("data-blocks-footer-link-columns-group-title", "")],
vec![text(heading_text)],
),
ul(vec![("class", "blocks-footer-link-columns-list")], items),
],
)
}
/// ブランド列(ロゴマーク + 名称 + 説明。`wide` で B 用の 2 列幅指定を
/// 付ける)。
fn brand_column(description: &'static str, wide: bool) -> Node {
let mut attrs = vec![("class", "blocks-footer-link-columns-brand")];
if wide {
attrs.push(("data-wide", ""));
}
div(
attrs,
vec![
div(
vec![("class", "blocks-footer-link-columns-brand-mark")],
vec![
logo_mark(),
heading::heading(
HeadingLevel::H3,
&HeadingProps::default(),
vec![],
vec![text("Fandhe Frontend")],
),
],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-footer-link-columns-brand-desc", "")],
vec![text(description)],
),
],
)
}
/// 著作権表示。
fn copyright() -> Node {
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-footer-link-columns-copyright", "")],
vec![text("\u{00a9} 2026 Fandhe Frontend")],
)
}
/// 注記行(各インスタンスの原案差分の要約)。
fn note(body: &'static str) -> Node {
styled_text::text(
&TextProps {
size: TextSize::Xs,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-footer-link-columns-note", "")],
vec![text(body)],
)
}
/// インスタンス A: 基準形(ブランド列 + リンク 4 群、下段に SNS アイコン
/// のみのリンク 1 個。X・Mastodon は実アカウントが無いため非表示)。
fn instance_a() -> Node {
footer(
vec![("class", "blocks-footer-link-columns-instance")],
vec![
note("基準形。ブランド列 + リンク 4 群。下段に SNS アイコンのみのリンク 1 個。"),
div(
vec![("class", "blocks-footer-link-columns-top")],
vec![
brand_column(
"決定的なビルドを目指す Rust 製フロントエンドフレームワークです。",
false,
),
div(
vec![("class", "blocks-footer-link-columns-columns")],
vec![
link_group(
"Product",
&[(GUIDE_URL, "Guide"), (API_REFERENCE_URL, "API Reference")],
),
link_group(
"Resources",
&[(EXAMPLES_URL, "Examples"), (RELEASES_URL, "Changelog")],
),
link_group(
"Community",
&[(REPO, "GitHub"), (DISCUSSIONS_URL, "Discussions")],
),
link_group("Company", &[(HOME_URL, "About")]),
],
),
],
),
separator::separator(
&SeparatorProps::default(),
vec![("data-blocks-footer-link-columns-separator", "")],
),
div(
vec![("class", "blocks-footer-link-columns-bottom")],
vec![
copyright(),
div(
vec![("class", "blocks-footer-link-columns-social")],
vec![social_icon_link(
REPO,
"M12 2L2 7l10 5 10-5-10-5z",
"GitHub",
)],
),
],
),
],
)
}
/// インスタンス B: ブランド列を強調(2 列幅、mission 文 + 架空の連絡先 +
/// SNS アイコン)。リンクは 2 群。下段は著作権のみ(Privacy Policy・
/// Terms of Service は実在ページが無いため非表示、モジュール doc
/// 「リンク先の方針」節参照)。
fn instance_b() -> Node {
footer(
vec![("class", "blocks-footer-link-columns-instance")],
vec![
note("ブランド列を 2 列幅に広げ、mission 文 + 連絡先 + SNS アイコンを収める。リンクは 2 群。下段は著作権のみ。"),
div(
vec![("class", "blocks-footer-link-columns-top"), ("data-brand", "wide")],
vec![
div(
vec![("class", "blocks-footer-link-columns-brand"), ("data-wide", "")],
vec![
div(
vec![("class", "blocks-footer-link-columns-brand-mark")],
vec![
logo_mark(),
heading::heading(
HeadingLevel::H3,
&HeadingProps::default(),
vec![],
vec![text("Fandhe Frontend")],
),
],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-footer-link-columns-brand-desc", "")],
vec![text(
"AI 時代のセキュリティリスク低減を目指し、プレーンな HTML/JS/CSS を尊重するフレームワークを開発しています。",
)],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-footer-link-columns-brand-contact", "")],
vec![text("123 Example Street, Sample City, ST 00000")],
),
div(
vec![("class", "blocks-footer-link-columns-social")],
vec![social_icon_link(REPO, "M12 2L2 7l10 5 10-5-10-5z", "GitHub")],
),
],
),
div(
vec![("class", "blocks-footer-link-columns-columns")],
vec![
link_group(
"Product",
&[(GUIDE_URL, "Guide"), (API_REFERENCE_URL, "API Reference")],
),
link_group("Company", &[(HOME_URL, "About")]),
],
),
],
),
separator::separator(
&SeparatorProps::default(),
vec![("data-blocks-footer-link-columns-separator", "")],
),
div(
vec![("class", "blocks-footer-link-columns-bottom")],
vec![copyright()],
),
],
)
}
/// インスタンス C: 下段なし。リンク 5 群(最後の Social 群はアイコン +
/// テキストのリンク 1 個。X は実アカウントが無いため非表示)。
fn instance_c() -> Node {
footer(
vec![("class", "blocks-footer-link-columns-instance")],
vec![
note("下段なし。リンク 5 群。最後の Social 群はアイコン + テキストのリンク 1 個。"),
div(
vec![("class", "blocks-footer-link-columns-top")],
vec![
brand_column("外部依存ゼロの描画コアを持つフレームワークです。", false),
div(
vec![("class", "blocks-footer-link-columns-columns")],
vec![
link_group(
"Product",
&[(GUIDE_URL, "Guide"), (API_REFERENCE_URL, "API Reference")],
),
link_group(
"Resources",
&[(EXAMPLES_URL, "Examples"), (RELEASES_URL, "Changelog")],
),
link_group("Community", &[(DISCUSSIONS_URL, "Discussions")]),
link_group("Company", &[(HOME_URL, "About")]),
social_link_group(),
],
),
],
),
],
)
}
/// C 用の Social 群(アイコン + テキストの `link`。`link_group` とは異なり
/// 各項目がアイコンを伴うため専用実装にする)。実在の遷移先を持つ
/// GitHub のみを掲載する(モジュール doc「リンク先の方針」節参照)。
fn social_link_group() -> Node {
let socials: [(&'static str, &'static str, &'static str); 1] =
[(REPO, "M12 2L2 7l10 5 10-5-10-5z", "GitHub")];
let items: Vec<Node> = socials
.iter()
.map(|(href, path_d, label)| {
li(
vec![],
vec![link::root(
href,
&LinkProps {
external: true,
..LinkProps::default()
},
vec![("data-blocks-footer-link-columns-social-text-link", "")],
vec![geo_icon(path_d, None), text(*label)],
)],
)
})
.collect();
div(
vec![("class", "blocks-footer-link-columns-group")],
vec![
heading::heading(
HeadingLevel::H4,
&HeadingProps::default(),
vec![("data-blocks-footer-link-columns-group-title", "")],
vec![text("Social")],
),
ul(vec![("class", "blocks-footer-link-columns-list")], items),
],
)
}
/// `footer-link-columns` の Demo 本体。呼び出しごとに同一の `Node` を返す
/// 純関数(他 block と同じ状態を持たない設計)。
#[must_use]
pub fn demo() -> Node {
div(
vec![("class", "blocks-footer-link-columns-layout")],
vec![instance_a(), instance_b(), instance_c()],
)
}原案差分メモ
集約元 7 件の差分を、次のとおり 3 インスタンスへ統合しています。
- A(基準形): ブランド列(ロゴマーク + 名称 + 一文説明)+ リンク 4 群。下段に区切り線・著作権・SNS アイコンのみのリンク 1 個(GitHub。表示名に対応する実在ページが無い X・Mastodon は掲載しない)
- B(ブランド列を強調): ブランド列を 2 列幅に広げ、mission 文・架空の連絡先・SNS アイコン(GitHub)を収める。リンクは 2 群。下段は著作権表示のみ(Privacy Policy・Terms of Service は実在ページが無いため掲載しない)
- C(下段なし): リンク 5 群。最後の「Social」群はアイコン + テキストのリンク。下段(区切り線・著作権)は持たない