Files
MyBlog-Next/src/layouts/PostDetails.astro
T
Sat Naing 76e15b55e3 feat: add Shiki transformers for better syntax highlighting (#534)
* refactor: organize prose related styles inside nested prose

* feat: add Shiki transformers for better syntax highlighting

* chore: update blog posts using Shiki transformers

Closes #210
2025-06-13 23:08:18 +07:00

273 lines
7.8 KiB
Plaintext

---
import { render, type CollectionEntry } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Tag from "@/components/Tag.astro";
import Datetime from "@/components/Datetime.astro";
import EditPost from "@/components/EditPost.astro";
import ShareLinks from "@/components/ShareLinks.astro";
import BackButton from "@/components/BackButton.astro";
import BackToTopButton from "@/components/BackToTopButton.astro";
import { getPath } from "@/utils/getPath";
import { slugifyStr } from "@/utils/slugify";
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
import IconChevronRight from "@/assets/icons/IconChevronRight.svg";
import { SITE } from "@/config";
export interface Props {
post: CollectionEntry<"blog">;
posts: CollectionEntry<"blog">[];
}
const { post, posts } = Astro.props;
const {
title,
author,
description,
ogImage: initOgImage,
canonicalURL,
pubDatetime,
modDatetime,
timezone,
tags,
hideEditPost,
} = post.data;
const { Content } = await render(post);
let ogImageUrl: string | undefined;
// Determine OG image source
if (typeof initOgImage === "string") {
ogImageUrl = initOgImage; // Remote OG image (absolute URL)
} else if (initOgImage?.src) {
ogImageUrl = initOgImage.src; // Local asset
}
// Use dynamic OG image if enabled and no remote|local ogImage
if (!ogImageUrl && SITE.dynamicOgImage) {
ogImageUrl = `${getPath(post.id, post.filePath)}/index.png`;
}
// Resolve OG image URL (or fallback to SITE.ogImage / default `og.png`)
const ogImage = ogImageUrl
? new URL(ogImageUrl, Astro.url.origin).href
: undefined;
const layoutProps = {
title: `${title} | ${SITE.title}`,
author,
description,
pubDatetime,
modDatetime,
canonicalURL,
ogImage,
scrollSmooth: true,
};
/* ========== Prev/Next Posts ========== */
const allPosts = posts.map(({ data: { title }, id, filePath }) => ({
id,
title,
filePath,
}));
const currentPostIndex = allPosts.findIndex(a => a.id === post.id);
const prevPost = currentPostIndex !== 0 ? allPosts[currentPostIndex - 1] : null;
const nextPost =
currentPostIndex !== allPosts.length ? allPosts[currentPostIndex + 1] : null;
---
<Layout {...layoutProps}>
<Header />
<BackButton />
<main
id="main-content"
class:list={[
"mx-auto w-full max-w-app px-4 pb-12",
{ "mt-8": !SITE.showBackButton },
]}
data-pagefind-body
>
<h1
transition:name={slugifyStr(title)}
class="inline-block text-2xl font-bold text-accent sm:text-3xl"
>
{title}
</h1>
<div class="flex items-center gap-4">
<Datetime {pubDatetime} {modDatetime} {timezone} size="lg" class="my-2" />
<EditPost class="max-sm:hidden" {hideEditPost} {post} />
</div>
<article
id="article"
class="mx-auto prose mt-8 max-w-app prose-pre:bg-(--shiki-light-bg) dark:prose-pre:bg-(--shiki-dark-bg)"
>
<Content />
</article>
<hr class="my-8 border-dashed" />
<EditPost class="sm:hidden" {hideEditPost} {post} />
<ul class="mt-4 mb-8 sm:my-8">
{tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} />)}
</ul>
<BackToTopButton />
<ShareLinks />
<hr class="my-6 border-dashed" />
<!-- Previous/Next Post Buttons -->
<div data-pagefind-ignore class="grid grid-cols-1 gap-6 sm:grid-cols-2">
{
prevPost && (
<a
href={getPath(prevPost.id, prevPost.filePath)}
class="flex w-full gap-1 hover:opacity-75"
>
<IconChevronLeft class="inline-block flex-none" />
<div>
<span>Previous Post</span>
<div class="text-sm text-accent/85">{prevPost.title}</div>
</div>
</a>
)
}
{
nextPost && (
<a
href={getPath(nextPost.id, nextPost.filePath)}
class="flex w-full justify-end gap-1 text-right hover:opacity-75 sm:col-start-2"
>
<div>
<span>Next Post</span>
<div class="text-sm text-accent/85">{nextPost.title}</div>
</div>
<IconChevronRight class="inline-block flex-none" />
</a>
)
}
</div>
</main>
<Footer />
</Layout>
<script is:inline data-astro-rerun>
/** Create a progress indicator
* at the top */
function createProgressBar() {
// Create the main container div
const progressContainer = document.createElement("div");
progressContainer.className =
"progress-container fixed top-0 z-10 h-1 w-full bg-background";
// Create the progress bar div
const progressBar = document.createElement("div");
progressBar.className = "progress-bar h-1 w-0 bg-accent";
progressBar.id = "myBar";
// Append the progress bar to the progress container
progressContainer.appendChild(progressBar);
// Append the progress container to the document body or any other desired parent element
document.body.appendChild(progressContainer);
}
createProgressBar();
/** Update the progress bar
* when user scrolls */
function updateScrollProgress() {
document.addEventListener("scroll", () => {
const winScroll =
document.body.scrollTop || document.documentElement.scrollTop;
const height =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
if (document) {
const myBar = document.getElementById("myBar");
if (myBar) {
myBar.style.width = scrolled + "%";
}
}
});
}
updateScrollProgress();
/** Attaches links to headings in the document,
* allowing sharing of sections easily */
function addHeadingLinks() {
const headings = Array.from(
document.querySelectorAll("h2, h3, h4, h5, h6")
);
for (const heading of headings) {
heading.classList.add("group");
const link = document.createElement("a");
link.className =
"heading-link ml-2 opacity-0 group-hover:opacity-100 focus:opacity-100";
link.href = "#" + heading.id;
const span = document.createElement("span");
span.ariaHidden = "true";
span.innerText = "#";
link.appendChild(span);
heading.appendChild(link);
}
}
addHeadingLinks();
/** Attaches copy buttons to code blocks in the document,
* allowing users to copy code easily. */
function attachCopyButtons() {
const copyButtonLabel = "Copy";
const codeBlocks = Array.from(document.querySelectorAll("pre"));
for (const codeBlock of codeBlocks) {
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
const copyButton = document.createElement("button");
copyButton.className =
"copy-code absolute right-3 -top-3 rounded bg-muted px-2 py-1 text-xs leading-4 text-foreground font-medium";
copyButton.innerHTML = copyButtonLabel;
codeBlock.setAttribute("tabindex", "0");
codeBlock.appendChild(copyButton);
// wrap codebock with relative parent element
codeBlock?.parentNode?.insertBefore(wrapper, codeBlock);
wrapper.appendChild(codeBlock);
copyButton.addEventListener("click", async () => {
await copyCode(codeBlock, copyButton);
});
}
async function copyCode(block, button) {
const code = block.querySelector("code");
const text = code?.innerText;
await navigator.clipboard.writeText(text ?? "");
// visual feedback that task is completed
button.innerText = "Copied";
setTimeout(() => {
button.innerText = copyButtonLabel;
}, 700);
}
}
attachCopyButtons();
/* Go to page start after page swap */
document.addEventListener("astro:after-swap", () =>
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
);
</script>