1248 字
6 分钟
为博客添加分享功能
2025-08-13

为什么要添加这个功能?#

平时有喜欢逛博客的习惯,偶然间发现一篇博文在你的博客上添加“一键发帖/分享文章至X/Twitter按钮”,发现博主用了Javascript代码实现的,但我的博客是用Astro框架搭建的,所以纯HTML/CSS/JS实现,下面是实现方法

分享到 Twitter 的实现方法#

1. 创建分享按钮组件#

src/components/目录下创建ShareButton.astro文件:

---
interface Props {
url?: string;
title?: string;
}
const { url, title }: Props = Astro.props;
function toAbsolute(u?: string): string {
try {
if (!u) return Astro.url?.href ?? '';
return new URL(u, Astro.url).href;
} catch {
return u ?? '';
}
}
const absoluteUrl = toAbsolute(url);
// 推荐语 + 标题
const promo = '发现一篇好文,分享给你🎉';
const shareText = title ? `${promo} ${title}` : promo;
// 使用 twitter.com/intent/tweet,并分别传 text 与 url
const intent = new URL('https://twitter.com/intent/tweet');
intent.searchParams.set('text', shareText);
if (absoluteUrl) intent.searchParams.set('url', absoluteUrl);
const xShareUrl = intent.toString();
---
<div class="x-share-root">
<!-- 无 JS 兜底链接(隐藏以避免被内容拦截规则影响可视布局) -->
<a href={xShareUrl} target="_blank" rel="noopener noreferrer" class="x-action-link" aria-hidden="true" tabindex="-1">X</a>
<!-- 可见触发元素:button,避免广告/社交拦截按 href 规则隐藏 -->
<button type="button" class="x-action-btn" data-share-text={shareText} data-share-url={xShareUrl} aria-label="转发到 X">
<span class="share-icon-bg" aria-hidden="true"></span>
</button>
</div>
<script is:inline>
// 移动端优先系统分享,失败回退到意图链接(按钮触发,避开拦截规则)
const container = document.currentScript?.previousElementSibling;
if (container) {
const btn = container.querySelector('.x-action-btn');
if (btn) {
btn.addEventListener('click', (e) => {
e.preventDefault();
const shareText = btn.getAttribute('data-share-text') || document.title;
const href = btn.getAttribute('data-share-url') || '#';
if (navigator.share) {
navigator.share({
title: document.title,
text: shareText,
url: window.location.href,
}).catch(() => {
window.open(href, '_blank', 'noopener,noreferrer');
});
} else {
window.open(href, '_blank', 'noopener,noreferrer');
}
}, { passive: false });
}
}
</script>
<style>
.x-action-btn {
/* 1. 容器样式:固定尺寸、圆形、Flex居中 */
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%; /* 圆形 */
/* 2. 视觉样式 */
background: #111;
color: #fff;
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.08),
0 1px 2px rgba(0, 0, 0, 0.06);
/* 3. 交互与兼容性 */
-webkit-tap-highlight-color: transparent;
position: relative;
z-index: 10;
flex: 0 0 auto; /* 防止在 Safari 的 flex 布局中被收缩 */
transition:
background-color .2s ease,
transform .15s ease,
box-shadow .2s ease;
cursor: pointer;
}
.x-action-btn:hover {
background: #000;
transform: translateY(-1px);
box-shadow:
0 4px 14px rgba(0, 0, 0, 0.12),
0 2px 6px rgba(0, 0, 0, 0.10);
}
.x-action-btn:active {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.x-action-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
}
/* 4. 图标样式:使用 Base64 背景图,兼容性最强 */
.share-icon-bg {
width: 20px;
height: 20px;
display: block;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjIzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiLz48L3N2Zz4=");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* 5. 容器与隐藏兜底链接 */
.x-share-root { display: inline-block; position: relative; }
.x-action-link {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
/* 6. 深色模式 */
@media (prefers-color-scheme: dark) {
.x-action-btn {
background: #0b0b0b;
border-color: rgba(255,255,255,0.12);
}
.x-action-btn:hover {
background: #000;
}
}
</style>

2. 在文章页面中使用#

在文章模板(如src/pages/blog/[slug].astro)中添加按钮:

---
import path from "node:path";
import License from "@components/misc/License.astro";
import Markdown from "@components/misc/Markdown.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from "@utils/content-utils";
import { getDir, getPostUrlBySlug } from "@utils/url-utils";
import { Icon } from "astro-icon/components";
import { licenseConfig } from "src/config";
import ImageWrapper from "../../components/misc/ImageWrapper.astro";
import PostMetadata from "../../components/PostMeta.astro";
import { profileConfig, siteConfig } from "../../config";
import { formatDateToYYYYMMDD } from "../../utils/date-utils";
import Giscus from '../../components/Giscus.astro';
import ShareButton from '../../components/ShareButton.astro';
export async function getStaticPaths() {
const blogEntries = await getSortedPosts();
return blogEntries.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
// 获取props对象
const props = Astro.props;
// 从props中获取entry
const entry = props.entry;
// 渲染文章内容
const renderResult = await entry.render();
// 从渲染结果中获取所需内容
const Content = renderResult.Content;
const headings = renderResult.headings;
const remarkPluginFrontmatter = renderResult.remarkPluginFrontmatter;
// 构建JSON-LD结构化数据
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: entry.data.title,
description: entry.data.description || entry.data.title,
keywords: entry.data.tags,
author: {
"@type": "Person",
name: profileConfig.name,
url: Astro.site,
},
datePublished: formatDateToYYYYMMDD(entry.data.published),
inLanguage: entry.data.lang
? entry.data.lang.replace("_", "-")
: siteConfig.lang.replace("_", "-"),
};
// 构建分享URL
const postUrl = new URL(Astro.url.pathname, Astro.site).href;
---
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}>
<script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative mb-4">
<div id="post-container" class:list={["card-base z-10 px-6 md:px-9 pt-6 pb-4 relative w-full ",
{}
]}>
<!-- word count and reading time -->
<div class="flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation">
<div class="flex flex-row items-center">
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
<Icon name="material-symbols:notes-rounded"></Icon>
</div>
<div class="text-sm">{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
</div>
<div class="flex flex-row items-center">
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
<Icon name="material-symbols:schedule-outline-rounded"></Icon>
</div>
<div class="text-sm">
{remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
</div>
</div>
</div>
<!-- title -->
<div class="relative onload-animation">
<div
data-pagefind-body data-pagefind-weight="10" data-pagefind-meta="title"
class="transition w-full block font-bold mb-3
text-3xl md:text-[2.25rem]/[2.75rem]
text-black/90 dark:text-white/90
md:before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
before:absolute before:top-[0.75rem] before:left-[-1.125rem]
">
{entry.data.title}
</div>
</div>
<!-- metadata -->
<div class="onload-animation">
<PostMetadata
class="mb-5"
published={entry.data.published}
updated={entry.data.updated}
tags={entry.data.tags}
category={entry.data.category}
></PostMetadata>
{!entry.data.image && <div class="border-[var(--line-divider)] border-dashed border-b-[1px] mb-5"></div>}
</div>
<!-- always show cover as long as it has one -->
{entry.data.image &&
<ImageWrapper id="post-cover" src={entry.data.image} basePath={path.join("content/posts/", getDir(entry.id))} class="mb-8 rounded-xl banner-container onload-animation"/>
}
<Markdown class="mb-6 markdown-content onload-animation">
<Content />
</Markdown>
<!-- 分享按钮 -->
<div class="mt-8 mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 relative" style="z-index: 1;">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-blue-800 dark:text-blue-200">分享这篇文章</span>
<ShareButton url={postUrl} title={entry.data.title} />
</div>
</div>
{licenseConfig.enable && <License title={entry.data.title} slug={entry.slug} pubDate={entry.data.published} class="mb-6 rounded-xl license-container onload-animation"></License>}
</div>
</div>
<div class="flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full">
<a href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}
class:list={["w-full font-bold overflow-hidden active:scale-95", {"pointer-events-none": !entry.data.nextSlug}]}>
{entry.data.nextSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-start gap-4" >
<Icon name="material-symbols:chevron-left-rounded" class="text-[2rem] text-[var(--primary)]" />
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
{entry.data.nextTitle}
</div>
</div>}
</a>
<a href={entry.data.prevSlug ? getPostUrlBySlug(entry.data.prevSlug) : "#"}
class:list={["w-full font-bold overflow-hidden active:scale-95", {"pointer-events-none": !entry.data.prevSlug}]}>
{entry.data.prevSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-end gap-4">
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
{entry.data.prevTitle}
</div>
<Icon name="material-symbols:chevron-right-rounded" class="text-[2rem] text-[var(--primary)]" />
</div>}
</a>
</div>
<!-- Giscus 评论组件 -->
<Giscus />
</MainGridLayout>

结果展示#

image-20250813150325546

点击分享,跳转到Twitter发布

image-20250813150423053

分享这篇文章
为博客添加分享功能
https://cyberzone.cloud/posts/为博客添加分享功能/
作者
suiyideali
发布于
2025-08-13
许可协议
CC BY-NC-SA 4.0