SanYeCao-blog/src/layouts/MarkdownPostLayout.astro

683 lines
16 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import BaseLayout from "./BaseLayout.astro";
import Remark42Embed from "@/components/Remark42Embed.astro";
import PostNav from "@/components/Posts/PostNav.astro";
import TagLink from "@/components/TagLink.astro";
import PostMenu from "@/components/Posts/PostMenu.astro";
import Dialog from "@/components/Posts/Dialog.astro";
import { fade } from "astro:transitions";
import { getTranslations } from "@/i18n";
import "@/styles/global.css";
const { post, frontmatter, lang, slug, postId, headings } = Astro.props;
const comments = lang === "zh" ? "评论区" : "comments";
const t = getTranslations(lang);
const publishedDate = new Intl.DateTimeFormat(
lang === "zh" ? "zh-CN" : "en-US",
{
year: "numeric",
month: "long",
day: "numeric",
},
).format(frontmatter.pubDate);
---
<BaseLayout
pageTitle=`${frontmatter.title} - ${t.banner.title}`
description={frontmatter.description}
image={frontmatter.image?.url}
>
<div class="post-layout page-shell">
<div id="reading-progress" aria-hidden="true"></div>
<a
href="/"
class="back-button"
id="back-button"
aria-label="Go back"
title="返回"
>
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M15 18l-6-6 6-6"></path>
</svg>
</a>
<PostMenu headings={headings} />
<article class="post-article">
<div class="post-header">
<div class="post-meta">
<h1 class="post-title" transition:name={`post-title-${postId}`}>
{frontmatter.title}
</h1>
<p class="description">
{frontmatter.description}
</p>
<p class="post-byline">
<span>{t.post.writtenBy} {frontmatter.author}</span>
<span class="byline-separator" aria-hidden="true">·</span>
<time datetime={frontmatter.pubDate.toISOString()}>
{t.post.publishedOn} {publishedDate}
</time>
</p>
<div class="tags" transition:name={`post-tags-${postId}`}>
{
frontmatter.tags.map((tag: string) => (
<TagLink tag={tag} lang={lang} />
))
}
</div>
</div>
<img
src={frontmatter.image.url}
alt={frontmatter.image.alt}
class="post-cover"
transition:name={`post-image-${postId}`}
/>
</div>
<div class="post-divider"></div>
<div class="post-content" transition:animate={fade({ duration: "0.2s" })}>
<slot />
</div>
<Dialog />
<PostNav post={post} lang={lang} />
<section
id="comments"
class="comments-section"
aria-labelledby="comments-heading"
>
<h2 id="comments-heading">{comments}</h2>
{t.post.commentsDesc.map((line: string) => (
<p class="comments-description" set:html={line} />
))}
<Remark42Embed slug={slug} />
</section>
</article>
</div>
</BaseLayout>
<script
type="application/ld+json"
set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: frontmatter.title,
description: frontmatter.description,
image: frontmatter.image
? [
new URL(
frontmatter.image.url ?? frontmatter.image,
Astro.site,
).toString(),
]
: undefined,
datePublished: frontmatter.pubDate,
dateModified: frontmatter.updatedDate ?? frontmatter.pubDate,
author: {
"@type": "Person",
name: "CLoverta",
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": new URL(Astro.url.pathname, Astro.site).toString(),
},
})}
/>
<script>
const initFloatingActions = () => {
const backButton = document.getElementById("back-button");
if (backButton && backButton.dataset.bound !== "true") {
const toggleBackButton = () => {
if (window.scrollY > 300) {
backButton.classList.add("show");
} else {
backButton.classList.remove("show");
}
};
backButton.addEventListener("click", () => {
if (window.history.length > 1) {
window.history.back();
} else {
window.location.href = "/";
}
});
window.addEventListener("scroll", toggleBackButton, {
passive: true,
});
toggleBackButton();
backButton.dataset.bound = "true";
}
};
const initReadingProgress = () => {
const readingWindow = window as typeof window & {
__postReadingProgressHandler?: () => void;
};
if (readingWindow.__postReadingProgressHandler) {
window.removeEventListener(
"scroll",
readingWindow.__postReadingProgressHandler,
);
window.removeEventListener(
"resize",
readingWindow.__postReadingProgressHandler,
);
}
const bar = document.getElementById("reading-progress");
const content = document.querySelector<HTMLElement>(".post-content");
if (!bar || !content) return;
// Keep the fixed progress bar outside Astro's animated page container.
// Otherwise the header's transformed compositor layer can paint above it.
if (bar.parentElement !== document.body) {
document.body.appendChild(bar);
}
const updateProgress = () => {
const contentTop = content.getBoundingClientRect().top + window.scrollY;
const readableDistance = Math.max(
content.scrollHeight - window.innerHeight,
1,
);
const progress = Math.min(
Math.max((window.scrollY - contentTop) / readableDistance, 0),
1,
);
bar.style.transform = `scaleX(${progress})`;
};
readingWindow.__postReadingProgressHandler = updateProgress;
window.addEventListener("scroll", updateProgress, { passive: true });
window.addEventListener("resize", updateProgress);
updateProgress();
};
initReadingProgress();
initFloatingActions();
document.addEventListener("astro:page-load", initReadingProgress);
document.addEventListener("astro:page-load", initFloatingActions);
</script>
<style>
.post-layout {
min-width: 0;
}
#reading-progress {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
width: 100%;
height: 3px;
background: #5a89ff;
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.08s linear;
pointer-events: none;
}
.back-button {
position: fixed;
top: 1.25rem;
left: 1.25rem;
width: 3rem;
height: 3rem;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
border-top: #536493 1.5px dotted;
border-left: #536493 1.5px dotted;
border-right: #536493 2.5px solid;
border-bottom: #536493 2.5px solid;
background: color-mix(in srgb, var(--background-color) 94%, transparent);
color: #222;
z-index: 1000;
visibility: hidden;
transform: translateY(-80px);
/* opacity: 0; */
transition:
opacity 0.2s ease,
transform 0.27s ease,
visibility 0.2s ease,
box-shadow 0.2s ease,
background 0.2s ease,
color 0.2s ease;
}
.back-button.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.back-button.show:hover {
transform: translateY(-2.5px);
box-shadow: 2px 3px 0px var(--deep-blue);
}
.back-button:hover {
transform: translateY(-2px);
}
:global(.dark) .back-button {
background: color-mix(
in srgb,
var(--background-color-dark) 94%,
transparent
);
color: #f5f5f5;
border-top-color: #515151;
border-left-color: #515151;
border-right-color: #515151;
border-bottom-color: #515151;
}
@media (max-width: 640px) {
.back-button {
top: 1rem;
left: 1rem;
width: 2.75rem;
height: 2.75rem;
}
}
.post-content {
--article-muted: #596672;
--article-rule: rgba(83, 100, 147, 0.25);
--article-tint: rgba(83, 100, 147, 0.045);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
font-size: 1.07rem;
font-weight: 400;
line-height: 1.85;
overflow-wrap: break-word;
}
.post-content :global(> :first-child) {
margin-top: 0;
}
.post-content :global(p) {
margin-block: 1.05em;
}
.post-content :global(h2) {
margin: 3.2rem 0 1rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: clamp(1.35rem, 2vw, 1.55rem);
line-height: 1.4;
text-wrap: balance;
}
.post-content :global(h3) {
margin: 2.25rem 0 0.8rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: clamp(1.12rem, 1.6vw, 1.28rem);
line-height: 1.45;
text-wrap: balance;
}
.post-content :global(h4) {
margin: 1.8rem 0 0.65rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
line-height: 1.5;
font-size: 1.05em;
}
.post-content :global(:is(h2, h3, h4) + :is(h3, h4, p, ul, ol)) {
margin-top: 0.7rem;
}
.post-content :global(ul),
.post-content :global(ol) {
margin-block: 1.1em;
padding-inline-start: 1.6em;
}
.post-content :global(li::marker) {
color: var(--article-muted);
font-variant-numeric: tabular-nums;
}
.post-content :global(li > p) {
margin-block: 0.45em;
}
.post-content :global(li > :is(ul, ol)) {
margin-block: 0.5em;
}
.post-content :global(li + li) {
margin-top: 0.35rem;
}
.post-content :global(img:not(.code-copy-icon)) {
display: block;
max-width: 100%;
height: auto;
margin-inline: auto;
}
.post-content :global(figure) {
margin: 2rem 0;
}
.post-content :global(figcaption) {
margin-top: 0.55rem;
color: var(--article-muted);
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: 0.78rem;
line-height: 1.5;
text-align: center;
}
.post-content :global(table) {
display: block;
max-width: 100%;
overflow-x: auto;
margin-block: 1.75rem;
border-collapse: collapse;
font-size: 0.9em;
line-height: 1.65;
}
.post-content :global(th),
.post-content :global(td) {
padding: 0.65rem 0.85rem;
border-bottom: 1px solid var(--article-rule);
vertical-align: top;
}
.post-content :global(th) {
background: var(--article-tint);
font-weight: 600;
}
.post-content :global(a) {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
}
.post-content :global(a:hover) {
text-decoration-color: currentColor;
}
.post-content :global(:is(a, summary):focus-visible) {
outline: 2px solid var(--deep-blue);
outline-offset: 3px;
}
.post-content :global(hr) {
margin-block: 2.5rem;
border: 0;
border-top: 1px dashed var(--article-rule);
}
.post-content :global(code),
.post-content :global(pre),
.post-content :global(kbd),
.post-content :global(samp) {
font-family: "Maple Mono", "Maple Mono CN", monospace;
}
a {
color: var(--url-color);
}
.post-header {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
align-items: start;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.post-meta {
min-width: 0;
}
.post-title {
margin: 0 0 0.5rem 0;
line-height: 1.25;
overflow-wrap: anywhere;
word-break: break-word;
text-wrap: balance;
}
.description {
margin: 0 0 0.7rem 0;
color: #596672;
font-weight: 400;
line-height: 1.6;
overflow-wrap: anywhere;
word-break: break-word;
}
.post-byline {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.3rem 0.55rem;
margin: 0.3rem 0;
color: #66717c;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: 0.8rem;
font-weight: 400;
line-height: 1.5;
overflow-wrap: anywhere;
word-break: break-word;
}
.byline-separator {
opacity: 0.65;
}
:global(html.dark) .post-byline {
color: #aab7c4;
}
:global(html.dark) .description {
color: #b5bec7;
}
@media (min-width: 701px) {
.post-title {
margin-bottom: 0.85rem;
font-size: clamp(1.7rem, 2.2vw, 2rem);
line-height: 1.4;
}
.description {
margin-bottom: 1rem;
font-size: 1rem;
}
}
.post-cover {
width: 300px;
height: auto;
aspect-ratio: 16 / 10;
object-fit: cover;
object-position: center;
display: block;
}
.post-divider {
border-top: 1px dashed #dfe4e9;
margin: 1.2rem 0 1.5rem;
}
:global(.dark) .post-divider {
border-top-color: #7f91a3;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.9rem;
margin-top: 0.8rem;
min-width: 0;
font-size: 0.88rem;
}
@media (max-width: 560px) {
.post-header {
grid-template-columns: 1fr;
}
.post-cover {
width: 100%;
max-width: 100%;
aspect-ratio: 16 / 9;
}
}
.post-content :global(details) {
margin: 1.5rem 0;
overflow: hidden;
border-top: 1px solid var(--article-rule);
border-bottom: 1px solid var(--article-rule);
border-radius: 0;
background: var(--article-tint);
}
.post-content :global(details summary) {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.8rem 0.9rem;
cursor: pointer;
font-weight: 600;
user-select: none;
list-style: none;
}
.post-content :global(details summary::-webkit-details-marker) {
display: none;
}
.post-content :global(details summary::before) {
content: "›";
display: inline-block;
font-size: 1.25rem;
line-height: 1;
transition: transform 0.2s ease;
}
.post-content :global(details[open] summary::before) {
transform: rotate(90deg);
}
/* 展开内容 */
.post-content :global(details > :not(summary)) {
box-sizing: border-box;
margin: 0.8rem 1rem;
padding: 0;
}
/* 为列表序号预留空间 */
.post-content :global(details > ol),
.post-content :global(details > ul) {
padding-inline-start: 1.6em;
}
.post-content :global(details li) {
margin: 0.8rem 0;
}
.post-content :global(blockquote) {
margin: 1.5rem 0;
padding: 0.15rem 0 0.15rem 1rem;
border-left: 3px solid var(--deep-blue);
color: var(--article-muted);
background: transparent;
border-radius: 0;
}
.post-content :global(blockquote p) {
margin: 0;
}
.post-content :global(blockquote p + p) {
margin-top: 0.75rem;
}
/* 块级公式:超出屏幕时只滚动公式,不撑宽整篇文章 */
.post-content :global(.katex-display) {
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
padding: 0.25rem 0;
-webkit-overflow-scrolling: touch;
}
/* 避免 KaTeX 内部元素强行把父容器撑宽 */
.post-content :global(.katex-display > .katex) {
min-width: max-content;
}
.comments-section {
scroll-margin-top: 5rem;
}
.comments-description {
color: #6b7680;
font-size: 0.85rem;
line-height: 1.6;
}
:global(html.dark) .comments-description {
color: #aab7c4;
}
:global(html.dark) .post-content {
--article-muted: #aab7c4;
--article-rule: rgba(170, 183, 196, 0.28);
--article-tint: rgba(170, 183, 196, 0.055);
}
:global(html.dark) .post-content :global(blockquote) {
border-left-color: #9ecbff;
}
@media (prefers-reduced-motion: reduce) {
#reading-progress {
transition: none;
}
}
</style>