Files
MyBlog-Next/src/pages/posts/[...slug]/index.astro
T
Sat Naing f0b644de3a feat!: AstroPaper v6 (#631)
* feat: add initial configuration and social icons for AstroPaper theme

* feat: add theme styles and design tokens

* chore: configure Astro with sitemap, i18n, fonts, and tooling

- Set site URL, i18n (single locale, no prefix), sitemap integration
- Add Google Sans Code font via stable fonts API
- Configure Shiki dual-theme markdown (min-light / night-owl)
- Fix astro-paper.config.ts to use relative import (alias not
  available when Vite loads root-level config files)
- Add @astrojs/sitemap and @typescript-eslint/parser dependencies
- Wire @typescript-eslint/parser into ESLint so TypeScript syntax
  in Astro frontmatter (type Props, etc.) parses correctly

* feat: add base styles, dark variant, and typography

- global.css: dark variant via [data-theme], base layer for * / a /
  button selectors, scrollbar tokens, layout utilities (max-w-app,
  app-layout), active-nav as @utility for Tailwind variant support
- html and body classes moved directly onto elements in BaseLayout
  (Tailwind-native approach) so only truly global selectors remain here
- typography.css: prose overrides, Shiki light/dark code block
  switching, diff/highlight/word-highlight transformer styles

* feat: add i18n types, English strings, and useTranslations helper

Plain typed object approach — no third-party i18n library needed.
Paraglide.js was considered but requires SSR output (hard blocker
for a static Astro site).

- UIStrings interface with nav, post, pagination, and a11y groups
- en.ts with `satisfies UIStrings` for literal-type preservation
- useTranslations(locale) helper falls back to English automatically

Adding a new language is a one-file addition; full routing stays
as a registry add-on.

* feat: add client-side theme toggle script

- Reads window.__theme.value set by the inline FOUC-prevention script
  to avoid re-detecting the theme on first load
- Toggles data-theme on <html> and persists to localStorage
- Fills <meta name="theme-color"> with the computed background colour
  so Android's browser chrome matches the page
- Re-runs setup on astro:after-swap for View Transitions navigation
- Carries theme-color across astro:before-swap to prevent navbar
  flicker on Android during page transitions
- Syncs with OS prefers-color-scheme changes at runtime

* feat: add BaseLayout and PostLayout

BaseLayout (replaces scaffold Layout.astro):
- Font, canonical URL via Astro.site, RSS autodiscovery
- FOUC-prevention inline script sets data-theme before first paint
  and exposes window.__theme.value for theme.ts to reuse
- html/body classes on elements directly (Tailwind-native)
- <slot name="head"> for child layout injection
- ClientRouter for View Transitions (respects lightAndDarkMode flag)
- googleVerification rendered from config instead of .env — it is a
  public string, not a secret, and not environment-specific

PostLayout (new):
- Wraps BaseLayout; injects article-specific head content via slot
- Overrides og:type to "article"
- Adds article:published_time / article:modified_time conditionally
- JSON-LD BlogPosting with conditional datePublished/dateModified
  (fixes old bug where absent dates produced "undefined" string)
- Structured data only on actual post pages (not home/archives/tags)

SiteConfig: add optional googleVerification field

* chore: replace scaffold index page with BaseLayout placeholder

* feat: add Header component

* feat: add Footer and Socials component

* chore: install dayjs, slugify, zod, @astrojs/mdx and add MDX integration

* feat: add resolved config that applies defaults in one place

Defaults for posts, features, socials, and shareLinks are merged in
src/config.ts so all consuming code gets non-optional types with no
scattered ?? fallbacks.

* feat: add content schema, blog path, and post utilities

Renames collection blog→posts, moves content to src/content/posts/,
adds .mdx support, and adds a typed pages collection. Includes
getSortedPosts, postFilter, getPath, and slugify utilities.

* feat: add Datetime and Card components

* refactor: switch layouts and components to resolved config

Replaces @/astro-paper.config imports with @/config and removes
optional chaining on feature flags now that the resolved config
guarantees non-optional values.

* feat: add home and footer i18n strings

* feat: implement home page with hero, featured, and recent posts

Replaces the BaseLayout placeholder with the full index page:
hero section with RSS link and social links, featured posts section,
recent posts capped at posts.perIndex, and an All Posts link. All
section labels come from i18n.

* feat: add pagination component

* feat: add Breadcrumb component with locale support

* feat: add Main component

* refactor: update code formatting in Card.astro

* feat: add posts page

* fix: add missing @astrojs/mdx dependency

* refactor: rename meta to pages in i18n

* refactor: improve code formatting in Breadcrumb

* refactor: destructure props in Main component for clarity

* feat: add tag pages

* feat: add About page

* chore: update i18n configuration to use site language from config

* feat: add Archives page

* feat: integrate Pagefind search functionality

* feat: add dynamic Open Graph image generation for posts

* feat: add post detail page

* feat: add Shiki transformers

* chore: remove unused edit link text

* chore: update .gitignore to include generated Pagefind directory

* chore: update site URL in configuration to production link

* chore: replace favicon and remove unused SVG assets, add new Open Graph image

* feat: update site config with default values

* feat: add default OG image path resolution

* fix: ensure edit post feature checks for enabled status correctly

* feat: implement dynamic Open Graph image generation

* feat: add RSS feed generation functionality

* feat: add 404 Not Found page

* feat: add robots.txt generation for SEO optimization

* refactor: change getSortedPosts to a named export

* refactor: change postFilter to a named export

* refactor: rename BaseLayout to Layout

* refactor: standardize locale handling across components

* refactor: update getPostsByGroupCondition location

* chore: remove unused Welcome component

* refactor(i18n): update links using getRelativeLocaleUrl

* refactor(i18n): add dynamic locale loading

* fix: update language attribute to use current locale

* fix: correct wording for updatedAt in post translations

* fix: update layout titles to use correct translation keys for tags pages

* chore: use new colors for better DX and a11y

* feat: add underline icon for active icon nav

* feat: add toc for markdown contents

* refactor: remove unused feature flags in config

* refactor(i18n): enhance locale handling and path management

* fix: ts unused variable hint

* refactor: extract locale stripping logic into a util func

* refactor(i18n): improve locale handling in tag pages

* refactor: update post URL helper functions

* refactor(i18n): replace pathWithBase with getRelativeLocaleUrl for locale handling

* refactor: rename fileWithBase to getAssetPath

* refactor: use runtime agnostic APIs for file paths

* refactor: use transition directive for heading transition

* refactor: remove unused title element from tag page

* fix: decouple ClientRouter from lightAndDarkMode flag

* refactor: rename `postPaths.ts` to `getPostPaths.ts`

* refactor: remove unused `minutesRead` string from post translations

* refactor: simplify AdjacentPostNav component by passing prevPost and nextPost directly

* refactor: remove unused code declaration

* docs: add existing blog posts

* chore: update astro and @astrojs/check dependencies to latest versions

* build: use experimental svg optimizer

* chore(repo): restore project meta files and Docker configs

* refactor: update code formatting with Prettier

* docs: update README and docs

* docs: add JSDoc comments to utility functions

* docs: add JSDoc comments to resolved config file

* fix(og): avoid double slashes when appending index.png

* chore(ci): update Node.js version to 22.12.0 in CI workflow

* feat: improve og image generation with Astro's getFontFileURL API (#632)

* feat: improve og image generation with Astro's getFontFileURL API

- remove @resvg/resvg-js and use sharp to convert SVG to PNG
- update og image generation to use Astro's getFontFileURL API for better font handling

* docs: update README and dynamic OG images documentation

- remove outdated content and clarify dynamic OG image generation process in README.
- add new image example and update instructions for handling non-Latin characters in dynamic OG images.
- adjust copyright year in README to 2026.

* fix(header): adjust icon button alignment in mobile

* feat(docs): add ResponsiveTable to use it in MDX posts (#634)

* feat(docs): add ResponsiveTable to use it in MDX posts

Introduce ResponsiveTable with variant-based styling. Rename docs posts to
.mdx, wrap tables in the component, and remove redundant table `code` rules
from prose typography.

* chore(prettier): ignore .mdx files when checking format

* chore(ci): update Node.js version to 24 in CI workflow

* bump: upgrade Astro and dependencies

* fix: remove redundant backUrl script bug

* fix(header): adjust button and icon alignment

* fix(a11y): improve muted-foreground color contrast

* docs: update predefined color schemes

* chore: support site verification env as fallback for googleVerification

* docs: add astro paper v6 updates in docs

* docs: add AstroPaper-v6.png in astro-paper-6 blog post

* fix: update back-to-top button DOM placement (#641)

Closes #626, #131, #493
2026-05-17 16:17:58 +07:00

266 lines
7.3 KiB
Plaintext

---
import { type CollectionEntry, getCollection, render } from "astro:content";
import PostLayout from "@/layouts/PostLayout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Datetime from "@/components/Datetime.astro";
import Tag from "@/components/Tag.astro";
import { getPostSlug, getPostUrl } from "@/utils/getPostPaths";
import { getSortedPosts } from "@/utils/getSortedPosts";
import { slugifyStr } from "@/utils/slugify";
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 AdjacentPostNav from "./_components/AdjacentPostNav.astro";
import config from "@/config";
export async function getStaticPaths() {
const posts = await getCollection("posts");
const sortedPosts = getSortedPosts(posts);
return sortedPosts.map((post, index) => ({
params: { slug: getPostSlug(post.id, post.filePath) },
props: {
post,
prevPost:
index > 0
? {
id: sortedPosts[index - 1].id,
title: sortedPosts[index - 1].data.title,
filePath: sortedPosts[index - 1].filePath,
}
: null,
nextPost:
index < sortedPosts.length - 1
? {
id: sortedPosts[index + 1].id,
title: sortedPosts[index + 1].data.title,
filePath: sortedPosts[index + 1].filePath,
}
: null,
},
}));
}
type AdjacentPost = {
id: string;
title: string;
filePath: string | undefined;
} | null;
type Props = {
post: CollectionEntry<"posts">;
prevPost: AdjacentPost;
nextPost: AdjacentPost;
};
const { post, prevPost, nextPost } = Astro.props;
const locale = Astro.currentLocale ?? config.site.lang;
const {
title,
description,
ogImage: initOgImage,
canonicalURL,
pubDatetime,
modDatetime,
timezone,
tags,
hideEditPost,
} = post.data;
const { Content } = await render(post);
let ogImageUrl: string | undefined;
if (typeof initOgImage === "string") {
ogImageUrl = initOgImage;
} else if (initOgImage?.src) {
ogImageUrl = initOgImage.src;
}
if (!ogImageUrl && config.features.dynamicOgImage) {
const postUrl = getPostUrl(post.id, post.filePath, locale).replace(
/\/+$/,
""
);
ogImageUrl = `${postUrl}/index.png`;
}
const ogImage = ogImageUrl
? new URL(ogImageUrl, Astro.url.origin).href
: undefined;
---
<PostLayout
title={`${title} | ${config.site.title}`}
description={description}
ogImage={ogImage}
canonicalURL={canonicalURL}
pubDatetime={pubDatetime}
modDatetime={modDatetime}
>
<Header />
<BackButton />
<main
id="main-content"
class:list={["app-layout", { "mt-8": !config.features.showBackButton }]}
data-pagefind-body
>
<h1
style={{ viewTransitionName: slugifyStr(title.replaceAll(".", "-")) }}
class="text-accent inline-block text-2xl font-bold sm:text-3xl"
>
{title}
</h1>
<div class="my-2 flex items-center gap-2">
<Datetime {pubDatetime} {modDatetime} {timezone} size="lg" />
<span
aria-hidden="true"
class:list={[
"text-muted-foreground max-sm:hidden",
{ hidden: !config.features.editPost?.enabled || hideEditPost },
]}
>
|
</span>
<EditPost {hideEditPost} {post} class="max-sm:hidden" />
</div>
<article
id="article"
class:list={[
"mt-8 w-full",
"app-prose 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} />
<BackToTopButton />
<ul class="mt-4 mb-8 flex flex-wrap gap-4 sm:my-8">
{tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} size="sm" />)}
</ul>
<ShareLinks />
<hr class="my-8 border-dashed" />
<AdjacentPostNav {prevPost} {nextPost} />
</main>
<Footer />
</PostLayout>
<script is:inline data-astro-rerun>
function createProgressBar() {
const progressContainer = document.createElement("div");
progressContainer.className =
"progress-container fixed top-0 z-10 h-1 w-full bg-background";
const progressBar = document.createElement("div");
progressBar.className = "progress-bar h-1 w-0 bg-accent";
progressBar.id = "myBar";
progressContainer.appendChild(progressBar);
document.body.appendChild(progressContainer);
}
createProgressBar();
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();
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 ms-2 no-underline opacity-75 md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100";
link.href = "#" + heading.id;
const span = document.createElement("span");
span.ariaHidden = "true";
span.innerText = "#";
link.appendChild(span);
heading.appendChild(link);
}
}
addHeadingLinks();
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 computedStyle = getComputedStyle(codeBlock);
const hasFileNameOffset =
computedStyle.getPropertyValue("--file-name-offset").trim() !== "";
const topClass = hasFileNameOffset
? "top-(--file-name-offset)"
: "-top-3";
const copyButton = document.createElement("button");
copyButton.className = `copy-code absolute end-3 ${topClass} rounded bg-muted border border-muted px-2 py-1 text-xs leading-4 text-foreground font-medium`;
copyButton.innerHTML = copyButtonLabel;
codeBlock.setAttribute("tabindex", "0");
codeBlock.appendChild(copyButton);
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 ?? "");
button.innerText = "Copied";
setTimeout(() => {
button.innerText = copyButtonLabel;
}, 700);
}
}
attachCopyButtons();
document.addEventListener("astro:after-swap", () =>
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
);
</script>