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
This commit is contained in:
Sat Naing
2026-05-17 16:17:58 +07:00
committed by GitHub
parent f3005328e5
commit f0b644de3a
122 changed files with 5387 additions and 4561 deletions
+12 -7
View File
@@ -3,10 +3,15 @@ import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import LinkButton from "@/components/LinkButton.astro";
import { SITE } from "@/config";
import { getRelativeLocaleUrl } from "astro:i18n";
import { useTranslations } from "@/i18n";
import config from "@/config";
const locale = Astro.currentLocale ?? config.site.lang;
const t = useTranslations(locale);
---
<Layout title={`404 Not Found | ${SITE.title}`}>
<Layout title={`${t.notFound.title} | ${config.site.title}`}>
<Header />
<main
@@ -14,14 +19,14 @@ import { SITE } from "@/config";
class="app-layout flex flex-1 items-center justify-center"
>
<div class="mb-14 flex flex-col items-center justify-center">
<h1 class="text-9xl font-bold text-accent">404</h1>
<span aria-hidden="true">¯\_(ツ)_/¯</span>
<p class="mt-4 text-2xl sm:text-3xl">Page Not Found</p>
<h1 class="text-accent text-9xl font-bold">404</h1>
<span aria-hidden="true"> ¯\_(ツ)_/¯ </span>
<p class="mt-4 text-2xl sm:text-3xl">{t.notFound.message}</p>
<LinkButton
href="/"
href={getRelativeLocaleUrl(locale, "")}
class="my-6 text-lg underline decoration-dashed underline-offset-8"
>
Go back home
{t.notFound.goHome}
</LinkButton>
</div>
</main>
+36
View File
@@ -0,0 +1,36 @@
---
import { getEntry, render } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import Main from "@/components/Main.astro";
import Footer from "@/components/Footer.astro";
import config from "@/config";
const about = await getEntry("pages", "about");
if (!about) {
throw new Error(
"Missing content entry: `about.md` or `about.mdx` in `src/content/pages/`"
);
}
const { Content } = await render(about);
---
<Layout
title={`${about.data.title} | ${config.site.title}`}
description={about.data.description}
ogImage={about.data.ogImage}
canonicalURL={about.data.canonicalURL}
>
<Header />
<Breadcrumb />
<Main pageTitle={about.data.title} class="app-prose">
<Content />
</Main>
<Footer />
</Layout>
-37
View File
@@ -1,37 +0,0 @@
---
layout: ../layouts/AboutLayout.astro
title: "About"
---
AstroPaper is a minimal, accessible and SEO-friendly blog theme built with [Astro](https://astro.build/) and [Tailwind CSS](https://tailwindcss.com/).
![Astro Paper](public/astropaper-og.jpg)
AstroPaper provides a solid foundation for blogs, or even portfolios\_ with full markdown support, built-in dark mode, and a clean layout that works out-of-the-box.
The blog posts in this theme also serve as guides, docs or example articles\_ making AstroPaper a flexible starting point for your next content-driven site.
## Features
AstroPaper comes with a set of useful features that make content publishing easy and effective:
- SEO-friendly
- Fast performance
- Light & dark mode
- Highly customizable
- Organizable blog posts
- Responsive & accessible
- Static search with [PageFind](https://pagefind.app/)
- Automatic social image generation
and so much more.
## Show your support
If you like [AstroPaper](https://github.com/satnaing/astro-paper), consider giving it a star ⭐️.
Found a bug 🐛 or have an improvement ✨ in mind? Feel free to open an [issue](https://github.com/satnaing/astro-paper/issues), submit a [pull request](https://github.com/satnaing/astro-paper/pulls) or start a [discussion](https://github.com/satnaing/astro-paper/discussions).
If you find this theme helpful, you can also [sponsor me on GitHub](https://github.com/sponsors/satnaing) or [buy me a coffee](https://buymeacoffee.com/satnaing) to show your support — every penny counts.
Kyay zuu! 🙏🏼
@@ -0,0 +1,24 @@
import type { CollectionEntry } from "astro:content";
type GroupKey = string | number | symbol;
type GroupFunction<T> = (item: T, index?: number) => GroupKey;
export function getPostsByGroupCondition(
posts: CollectionEntry<"posts">[],
groupFunction: GroupFunction<CollectionEntry<"posts">>
) {
const result: Record<GroupKey, CollectionEntry<"posts">[]> = {};
for (let i = 0; i < posts.length; i++) {
const item = posts[i];
const groupKey = groupFunction(item, i);
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
}
return result;
}
+32 -26
View File
@@ -1,42 +1,41 @@
---
import { getRelativeLocaleUrl } from "astro:i18n";
import { getCollection } from "astro:content";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import Main from "@/components/Main.astro";
import Footer from "@/components/Footer.astro";
import Card from "@/components/Card.astro";
import getPostsByGroupCondition from "@/utils/getPostsByGroupCondition";
import { SITE } from "@/config";
import { postFilter } from "@/utils/postFilter";
import { getPostsByGroupCondition } from "./_utils/getPostsByGroupCondition";
import { useTranslations } from "@/i18n";
import config from "@/config";
const locale = Astro.currentLocale ?? config.site.lang;
// Redirect to 404 page if `showArchives` config is false
if (!SITE.showArchives) {
return Astro.redirect("/404");
const notFoundUrl = getRelativeLocaleUrl(locale, "404");
if (!config.features.showArchives && notFoundUrl) {
return Astro.rewrite(notFoundUrl);
}
const posts = await getCollection("blog", ({ data }) => !data.draft);
const t = useTranslations(locale);
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const posts = await getCollection("posts");
const filteredPosts = posts.filter(postFilter);
const monthFormatter = new Intl.DateTimeFormat(locale, { month: "long" });
---
<Layout title={`Archives | ${SITE.title}`}>
<Layout title={`${t.pages.archivesTitle} | ${config.site.title}`}>
<Header />
<Main pageTitle="Archives" pageDesc="All the articles I've archived.">
<Breadcrumb />
<Main pageTitle={t.pages.archivesTitle} pageDesc={t.pages.archivesDesc}>
{
Object.entries(
getPostsByGroupCondition(posts, post =>
getPostsByGroupCondition(filteredPosts, post =>
post.data.pubDatetime.getFullYear()
)
)
@@ -44,7 +43,7 @@ const months = [
.map(([year, yearGroup]) => (
<div>
<span class="text-2xl font-bold">{year}</span>
<sup class="text-sm">{yearGroup.length}</sup>
<sup class="text-muted-foreground text-sm">{yearGroup.length}</sup>
{Object.entries(
getPostsByGroupCondition(
yearGroup,
@@ -55,8 +54,14 @@ const months = [
.map(([month, monthGroup]) => (
<div class="flex flex-col sm:flex-row">
<div class="mt-6 min-w-36 text-lg sm:my-6">
<span class="font-bold">{months[Number(month) - 1]}</span>
<sup class="text-xs">{monthGroup.length}</sup>
<span class="font-bold">
{monthFormatter.format(
new Date(2000, Number(month) - 1, 1)
)}
</span>
<sup class="text-muted-foreground text-xs">
{monthGroup.length}
</sup>
</div>
<ul>
{monthGroup
@@ -79,5 +84,6 @@ const months = [
))
}
</Main>
<Footer />
</Layout>
+44 -28
View File
@@ -1,42 +1,53 @@
---
import { getCollection } from "astro:content";
import { getRelativeLocaleUrl } from "astro:i18n";
import IconRss from "@/assets/icons/IconRss.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Socials from "@/components/Socials.astro";
import LinkButton from "@/components/LinkButton.astro";
import Card from "@/components/Card.astro";
import getSortedPosts from "@/utils/getSortedPosts";
import IconRss from "@/assets/icons/IconRss.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import { SITE } from "@/config";
import { SOCIALS } from "@/constants";
import LinkButton from "@/components/LinkButton.astro";
import { getSortedPosts } from "@/utils/getSortedPosts";
import { useTranslations } from "@/i18n";
import config from "@/config";
const posts = await getCollection("blog");
const { socials, posts: postsConfig } = config;
const locale = Astro.currentLocale ?? config.site.lang;
const t = useTranslations(locale);
const posts = await getCollection("posts");
const sortedPosts = getSortedPosts(posts);
const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
const homePath = getRelativeLocaleUrl(locale, "");
---
<Layout>
<Header />
<main id="main-content" data-layout="index" class="app-layout">
<section id="hero" class:list={["pt-8 pb-6", "border-b border-border"]}>
<main
id="main-content"
data-layout="index"
data-home-path={homePath}
class="app-layout"
>
<section id="hero" class="border-border border-b pt-8 pb-6">
<h1 class="my-4 inline-block text-4xl font-bold sm:my-8 sm:text-5xl">
Mingalaba
</h1>
<a
target="_blank"
href="/rss.xml"
href={getRelativeLocaleUrl(locale, "rss.xml")}
class="inline-block"
aria-label="rss feed"
aria-label="RSS Feed"
title="RSS Feed"
>
<IconRss
width={20}
height={20}
class="scale-125 stroke-accent stroke-3 rtl:-rotate-90"
class="stroke-accent scale-125 stroke-3 rtl:-rotate-90"
/>
<span class="sr-only">RSS Feed</span>
</a>
@@ -50,17 +61,18 @@ const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
<p class="mt-2">
Read the blog posts or check
<LinkButton
class="underline decoration-dashed underline-offset-4 hover:text-accent"
class="hover:text-accent underline decoration-dashed underline-offset-4"
href="https://github.com/satnaing/astro-paper#readme"
>
README
</LinkButton> for more info.
</p>
{
// only display if at least one social link is enabled
SOCIALS.length > 0 && (
socials.length > 0 && (
<div class="mt-4 flex max-sm:flex-col sm:items-center">
<div class="me-2 mb-1 whitespace-nowrap sm:mb-0">Social Links:</div>
<div class="me-2 mb-1 whitespace-nowrap sm:mb-0">
{t.home.socialLinks}:
</div>
<Socials />
</div>
)
@@ -73,10 +85,12 @@ const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
id="featured"
class:list={[
"pt-12 pb-6",
{ "border-b border-border": recentPosts.length > 0 },
{ "border-border border-b": recentPosts.length > 0 },
]}
>
<h2 class="text-2xl font-semibold tracking-wide">Featured</h2>
<h2 class="text-2xl font-semibold tracking-wide">
{t.home.featured}
</h2>
<ul>
{featuredPosts.map(data => (
<Card variant="h3" {...data} />
@@ -89,20 +103,21 @@ const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
{
recentPosts.length > 0 && (
<section id="recent-posts" class="pt-12 pb-6">
<h2 class="text-2xl font-semibold tracking-wide">Recent Posts</h2>
<h2 class="text-2xl font-semibold tracking-wide">
{t.home.recentPosts}
</h2>
<ul>
{recentPosts.map(
(data, index) =>
index < SITE.postPerIndex && <Card variant="h3" {...data} />
)}
{recentPosts.slice(0, postsConfig.perIndex).map(data => (
<Card variant="h3" {...data} />
))}
</ul>
</section>
)
}
<div class="my-8 text-center">
<LinkButton href="/posts/">
All Posts
<LinkButton href={getRelativeLocaleUrl(locale, "posts")}>
{t.home.allPosts}
<IconArrowRight class="inline-block rtl:-rotate-180" />
</LinkButton>
</div>
@@ -113,9 +128,10 @@ const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
<script>
document.addEventListener("astro:page-load", () => {
const indexLayout = (document.querySelector("#main-content") as HTMLElement)
?.dataset?.layout;
if (indexLayout) {
sessionStorage.setItem("backUrl", "/");
?.dataset;
const baseRoot = import.meta.env.BASE_URL.replace(/\/?$/, "/");
if (indexLayout?.layout) {
sessionStorage.setItem("backUrl", indexLayout.homePath ?? baseRoot);
}
});
</script>
+163 -4
View File
@@ -1,9 +1,168 @@
import type { APIRoute } from "astro";
import { generateOgImageForSite } from "@/utils/generateOgImages";
import satori from "satori";
import sharp from "sharp";
import { fontData, experimental_getFontFileURL } from "astro:assets";
import { getFontPathByWeight } from "@/utils/getFontPathByWeight";
import config from "@/config";
export const GET: APIRoute = async () => {
const buffer = await generateOgImageForSite();
return new Response(new Uint8Array(buffer), {
export const GET: APIRoute = async context => {
const fonts = fontData["--font-google-sans-code"];
const regularFontPath = getFontPathByWeight(fonts, 400);
const boldFontPath = getFontPathByWeight(fonts, 700);
if (regularFontPath === undefined || boldFontPath === undefined) {
throw new Error("Cannot find the font path.");
}
const [regularData, boldData] = await Promise.all([
fetch(experimental_getFontFileURL(regularFontPath, context.url)).then(res =>
res.arrayBuffer()
),
fetch(experimental_getFontFileURL(boldFontPath, context.url)).then(res =>
res.arrayBuffer()
),
]);
const svg = await satori(
{
type: "div",
props: {
style: {
background: "#fefbfb",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "Google Sans Code",
},
children: [
{
type: "div",
props: {
style: {
position: "absolute",
top: "-1px",
right: "-1px",
border: "4px solid #000",
background: "#ecebeb",
opacity: "0.9",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
margin: "2.5rem",
width: "88%",
height: "80%",
},
},
},
{
type: "div",
props: {
style: {
border: "4px solid #000",
background: "#fefbfb",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
margin: "2rem",
width: "88%",
height: "80%",
},
children: {
type: "div",
props: {
style: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
margin: "20px",
width: "90%",
height: "90%",
},
children: [
{
type: "div",
props: {
style: {
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "90%",
maxHeight: "90%",
overflow: "hidden",
textAlign: "center",
},
children: [
{
type: "p",
props: {
style: { fontSize: 72, fontWeight: "bold" },
children: config.site.title,
},
},
{
type: "p",
props: {
style: { fontSize: 28 },
children: config.site.description,
},
},
],
},
},
{
type: "div",
props: {
style: {
display: "flex",
justifyContent: "flex-end",
width: "100%",
marginBottom: "8px",
fontSize: 28,
},
children: {
type: "span",
props: {
style: { overflow: "hidden", fontWeight: "bold" },
children: new URL(config.site.url).hostname,
},
},
},
},
],
},
},
},
},
],
},
},
{
width: 1200,
height: 630,
embedFont: true,
fonts: [
{
name: "Google Sans Code",
data: regularData,
weight: 400,
style: "normal",
},
{
name: "Google Sans Code",
data: boldData,
weight: 700,
style: "normal",
},
],
}
);
const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
return new Response(new Uint8Array(pngBuffer), {
headers: { "Content-Type": "image/png" },
});
};
+16 -8
View File
@@ -1,26 +1,34 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import Main from "@/components/Main.astro";
import Card from "@/components/Card.astro";
import Footer from "@/components/Footer.astro";
import Pagination from "@/components/Pagination.astro";
import getSortedPosts from "@/utils/getSortedPosts";
import { SITE } from "@/config";
import { getSortedPosts } from "@/utils/getSortedPosts";
import { useTranslations } from "@/i18n";
import config from "@/config";
export const getStaticPaths = (async ({ paginate }) => {
const posts = await getCollection("blog", ({ data }) => !data.draft);
return paginate(getSortedPosts(posts), { pageSize: SITE.postPerPage });
const posts = await getCollection("posts", ({ data }) => !data.draft);
return paginate(getSortedPosts(posts), { pageSize: config.posts.perPage });
}) satisfies GetStaticPaths;
const { page } = Astro.props;
const locale = Astro.currentLocale ?? config.site.lang;
const t = useTranslations(locale);
---
<Layout title={`Posts | ${SITE.title}`}>
<Layout title={`${t.pages.postsTitle} | ${config.site.title}`}>
<Header />
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
<Breadcrumb />
<Main pageTitle={t.pages.postsTitle} pageDesc={t.pages.postsDesc}>
<ul>
{page.data.map(data => <Card {...data} />)}
</ul>
@@ -0,0 +1,53 @@
---
import IconArrowLeft from "@/assets/icons/IconArrowLeft.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import { getPostUrl } from "@/utils/getPostPaths";
import { useTranslations } from "@/i18n";
import config from "@/config";
type AdjacentPost = {
id: string;
title: string;
filePath: string | undefined;
} | null;
type Props = {
prevPost: AdjacentPost;
nextPost: AdjacentPost;
};
const { prevPost, nextPost } = Astro.props;
const locale = Astro.currentLocale ?? config.site.lang;
const t = useTranslations(locale);
---
<div data-pagefind-ignore class="my-8 grid grid-cols-1 gap-6 sm:grid-cols-2">
{
prevPost && (
<a
href={getPostUrl(prevPost.id, prevPost.filePath, locale)}
class="flex w-full gap-1 hover:opacity-75"
>
<IconArrowLeft class="inline-block flex-none rtl:rotate-180" />
<div>
<span>{t.post.previousPost}</span>
<div class="text-accent/85 text-sm">{prevPost.title}</div>
</div>
</a>
)
}
{
nextPost && (
<a
href={getPostUrl(nextPost.id, nextPost.filePath, locale)}
class="flex w-full justify-end gap-1 text-end hover:opacity-75 sm:col-start-2"
>
<div>
<span>{t.post.nextPost}</span>
<div class="text-accent/85 text-sm">{nextPost.title}</div>
</div>
<IconArrowRight class="inline-block flex-none rtl:rotate-180" />
</a>
)
}
</div>
@@ -0,0 +1,41 @@
---
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
import LinkButton from "@/components/LinkButton.astro";
import { getRelativeLocaleUrl } from "astro:i18n";
import { useTranslations } from "@/i18n";
import config from "@/config";
const locale = Astro.currentLocale ?? config.site.lang;
const t = useTranslations(locale);
---
{
config.features.showBackButton && (
<div class="app-layout flex items-center justify-start">
<LinkButton
id="back-button"
href={getRelativeLocaleUrl(locale, "")}
class="focus-outline hover:text-foreground/75 -ms-2 mt-8 mb-2"
>
<IconChevronLeft class="inline-block size-6 rtl:rotate-180" />
<span>{t.post.goBack}</span>
</LinkButton>
</div>
)
}
<script>
function updateGoBackUrl() {
const backButton: HTMLAnchorElement | null =
document.querySelector("#back-button");
const backUrl = sessionStorage.getItem("backUrl");
if (backUrl && backButton) {
backButton.href = backUrl;
}
}
document.addEventListener("astro:page-load", updateGoBackUrl);
updateGoBackUrl();
</script>
@@ -0,0 +1,87 @@
---
import IconArrowLeft from "@/assets/icons/IconArrowLeft.svg";
import IconArrowNarrowUp from "@/assets/icons/IconArrowNarrowUp.svg";
import { useTranslations } from "@/i18n";
const t = useTranslations(Astro.currentLocale);
---
<div
id="btt-btn-container"
class:list={[
"fixed inset-e-4 bottom-8 z-50",
"md:sticky md:inset-e-auto md:float-end md:me-1",
"translate-y-14 opacity-0 transition duration-500",
]}
>
<button
data-button="back-to-top"
class:list={[
"group bg-background relative px-2 py-1",
"size-14 rounded-full shadow-xl",
"md:h-8 md:w-fit md:rounded-md md:shadow-none md:focus-visible:rounded-none",
"md:bg-background/35 md:bg-clip-padding md:backdrop-blur-lg",
]}
>
<span
id="progress-indicator"
class="absolute inset-0 -z-10 block size-14 scale-110 rounded-full bg-transparent md:hidden md:h-8 md:rounded-md"
></span>
<IconArrowLeft class="inline-block rotate-90 md:hidden" />
<span class="group-hover:text-accent sr-only text-sm md:not-sr-only">
<IconArrowNarrowUp class="inline-block size-4" />
{t.post.backToTop}
</span>
</button>
</div>
<script is:inline data-astro-rerun>
function backToTop() {
const rootElement = document.documentElement;
const btnContainer = document.querySelector("#btt-btn-container");
const backToTopBtn = document.querySelector("[data-button='back-to-top']");
const progressIndicator = document.querySelector("#progress-indicator");
if (!rootElement || !btnContainer || !backToTopBtn || !progressIndicator)
return;
backToTopBtn.addEventListener("click", () => {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
});
let lastVisible = null;
function handleScroll() {
const scrollTotal = rootElement.scrollHeight - rootElement.clientHeight;
const scrollTop = rootElement.scrollTop;
const scrollPercent = Math.floor((scrollTop / scrollTotal) * 100);
progressIndicator.style.setProperty(
"background-image",
`conic-gradient(var(--accent), var(--accent) ${scrollPercent}%, transparent ${scrollPercent}%)`
);
const isVisible = scrollTop / scrollTotal > 0.3;
if (isVisible !== lastVisible) {
btnContainer.classList.toggle("opacity-100", isVisible);
btnContainer.classList.toggle("translate-y-0", isVisible);
btnContainer.classList.toggle("opacity-0", !isVisible);
btnContainer.classList.toggle("translate-y-14", !isVisible);
lastVisible = isVisible;
}
}
let ticking = false;
document.addEventListener("scroll", () => {
if (!ticking) {
window.requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
});
}
backToTop();
</script>
@@ -0,0 +1,37 @@
---
import type { CollectionEntry } from "astro:content";
import IconEdit from "@/assets/icons/IconEdit.svg";
import { useTranslations } from "@/i18n";
import config from "@/config";
type Props = {
hideEditPost?: CollectionEntry<"posts">["data"]["hideEditPost"];
class?: string;
post: CollectionEntry<"posts">;
};
const { hideEditPost, post, class: className = "" } = Astro.props;
const editPost = config.features.editPost;
const href = editPost.enabled ? `${editPost.url}${post.filePath}` : "";
const showEditPost = editPost.enabled && !hideEditPost && href.trim() !== "";
const t = useTranslations(Astro.currentLocale);
---
{
showEditPost && (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
class:list={[
"hover:text-accent text-muted-foreground flex justify-baseline gap-1.5",
className,
]}
>
<IconEdit class="inline-block" />
<span>{t.post.editPage}</span>
</a>
)
}
@@ -0,0 +1,54 @@
---
import LinkButton from "@/components/LinkButton.astro";
import { tplStr, useTranslations } from "@/i18n";
import config from "@/config";
const { shareLinks } = config;
const t = useTranslations(Astro.currentLocale);
const icons = import.meta.glob<{
default: (_props: Record<string, unknown>) => unknown;
}>("/src/assets/icons/socials/*.svg");
const pageUrl = Astro.url;
const platformLabel = (name: string) =>
name.charAt(0).toUpperCase() + name.slice(1);
const items = await Promise.all(
shareLinks.map(async ({ name, url, linkTitle }) => {
const mod = await icons[`/src/assets/icons/socials/${name}.svg`]?.();
const Icon = mod?.default;
const title =
linkTitle ??
(name === "mail"
? t.post.sharePostViaEmail
: tplStr(t.post.sharePostOn, { platform: platformLabel(name) }));
return { url, title, Icon };
})
);
---
{
items.some(({ Icon }) => Icon) && (
<div class="flex flex-none flex-col items-center justify-center gap-1 md:items-start">
<span class="italic">{t.post.sharePostIntro}</span>
<div class="text-center">
{items.map(({ url, title, Icon }) =>
Icon ? (
<LinkButton
href={`${url}${pageUrl}`}
class="scale-90 p-2 hover:rotate-6 sm:p-1"
title={title}
target="_blank"
rel="noopener noreferrer"
>
<Icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
<span class="sr-only">{title}</span>
</LinkButton>
) : null
)}
</div>
</div>
)
}
+256 -18
View File
@@ -1,27 +1,265 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import PostDetails from "@/layouts/PostDetails.astro";
import getSortedPosts from "@/utils/getSortedPosts";
import { getPath } from "@/utils/getPath";
type Props = {
post: CollectionEntry<"blog">;
};
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("blog", ({ data }) => !data.draft);
const postResult = posts.map(post => ({
params: { slug: getPath(post.id, post.filePath, false) },
props: { post },
}));
const posts = await getCollection("posts");
const sortedPosts = getSortedPosts(posts);
return postResult;
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,
},
}));
}
const { post } = Astro.props;
type AdjacentPost = {
id: string;
title: string;
filePath: string | undefined;
} | null;
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
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;
---
<PostDetails post={post} posts={sortedPosts} />
<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>
+177 -15
View File
@@ -1,34 +1,196 @@
import type { APIRoute } from "astro";
import { getCollection, type CollectionEntry } from "astro:content";
import { getPath } from "@/utils/getPath";
import { generateOgImageForPost } from "@/utils/generateOgImages";
import { SITE } from "@/config";
import { getCollection } from "astro:content";
import { fontData, experimental_getFontFileURL } from "astro:assets";
import satori from "satori";
import sharp from "sharp";
import { getFontPathByWeight } from "@/utils/getFontPathByWeight";
import { getPostSlug } from "@/utils/getPostPaths";
import config from "@/config";
export async function getStaticPaths() {
if (!SITE.dynamicOgImage) {
if (!config.features.dynamicOgImage) {
return [];
}
const posts = await getCollection("blog").then(p =>
const posts = await getCollection("posts").then(p =>
p.filter(({ data }) => !data.draft && !data.ogImage)
);
return posts.map(post => ({
params: { slug: getPath(post.id, post.filePath, false) },
params: { slug: getPostSlug(post.id, post.filePath) },
props: post,
}));
}
export const GET: APIRoute = async ({ props }) => {
if (!SITE.dynamicOgImage) {
return new Response(null, {
status: 404,
statusText: "Not found",
});
export const GET: APIRoute = async ({ props, url }) => {
if (!config.features.dynamicOgImage) {
return new Response(null, { status: 404, statusText: "Not found" });
}
const buffer = await generateOgImageForPost(props as CollectionEntry<"blog">);
return new Response(new Uint8Array(buffer), {
const fonts = fontData["--font-google-sans-code"];
const regularFontPath = getFontPathByWeight(fonts, 400);
const boldFontPath = getFontPathByWeight(fonts, 700);
if (regularFontPath === undefined || boldFontPath === undefined) {
throw new Error("Cannot find the font path.");
}
const [regularData, boldData] = await Promise.all([
fetch(experimental_getFontFileURL(regularFontPath, url)).then(res =>
res.arrayBuffer()
),
fetch(experimental_getFontFileURL(boldFontPath, url)).then(res =>
res.arrayBuffer()
),
]);
const svg = await satori(
{
type: "div",
props: {
style: {
background: "#fefbfb",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
children: [
{
type: "div",
props: {
style: {
position: "absolute",
top: "-1px",
right: "-1px",
border: "4px solid #000",
background: "#ecebeb",
opacity: "0.9",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
margin: "2.5rem",
width: "88%",
height: "80%",
},
},
},
{
type: "div",
props: {
style: {
border: "4px solid #000",
background: "#fefbfb",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
margin: "2rem",
width: "88%",
height: "80%",
},
children: {
type: "div",
props: {
style: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
margin: "20px",
width: "90%",
height: "90%",
},
children: [
{
type: "p",
props: {
style: {
fontSize: 72,
fontWeight: "bold",
maxHeight: "84%",
overflow: "hidden",
},
children: props.data.title,
},
},
{
type: "div",
props: {
style: {
display: "flex",
justifyContent: "space-between",
width: "100%",
marginBottom: "8px",
fontSize: 28,
},
children: [
{
type: "span",
props: {
children: [
"by ",
{
type: "span",
props: {
style: { color: "transparent" },
children: '"',
},
},
{
type: "span",
props: {
style: {
overflow: "hidden",
fontWeight: "bold",
},
children: props.data.author,
},
},
],
},
},
{
type: "span",
props: {
style: { overflow: "hidden", fontWeight: "bold" },
children: config.site.title,
},
},
],
},
},
],
},
},
},
},
],
},
},
{
width: 1200,
height: 630,
embedFont: true,
fonts: [
{
name: "Google Sans Code",
data: regularData,
weight: 400,
style: "normal",
},
{
name: "Google Sans Code",
data: boldData,
weight: 700,
style: "normal",
},
],
}
);
const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
return new Response(new Uint8Array(pngBuffer), {
headers: { "Content-Type": "image/png" },
});
};
+9 -8
View File
@@ -1,18 +1,19 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import { getPath } from "@/utils/getPath";
import getSortedPosts from "@/utils/getSortedPosts";
import { SITE } from "@/config";
import { getSortedPosts } from "@/utils/getSortedPosts";
import { getPostUrl } from "@/utils/getPostPaths";
import config from "@/config";
export async function GET() {
const posts = await getCollection("blog");
const posts = await getCollection("posts");
const sortedPosts = getSortedPosts(posts);
return rss({
title: SITE.title,
description: SITE.desc,
site: SITE.website,
title: config.site.title,
description: config.site.description,
site: config.site.url,
items: sortedPosts.map(({ data, id, filePath }) => ({
link: getPath(id, filePath),
link: getPostUrl(id, filePath, config.site.lang),
title: data.title,
description: data.description,
pubDate: new Date(data.modDatetime ?? data.pubDatetime),
+41 -8
View File
@@ -1,19 +1,47 @@
---
import { getRelativeLocaleUrl } from "astro:i18n";
import "@pagefind/default-ui/css/ui.css";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import Main from "@/components/Main.astro";
import Footer from "@/components/Footer.astro";
import { SITE } from "@/config";
import { getAssetPath } from "@/utils/withBase";
import { useTranslations } from "@/i18n";
import config from "@/config";
const backUrl = SITE.showBackButton ? `${Astro.url.pathname}` : "/";
const locale = Astro.currentLocale ?? config.site.lang;
// Redirect to 404 page if `search` feature is not enabled
const notFoundUrl = getRelativeLocaleUrl(locale, "404");
if (config.features.search !== "pagefind" && notFoundUrl) {
return Astro.rewrite(notFoundUrl);
}
const backUrl = config.features.showBackButton
? `${Astro.url.pathname}`
: getRelativeLocaleUrl(locale, "");
const pagefindBundlePath = getAssetPath("pagefind/");
const t = useTranslations(locale);
---
<Layout title={`Search | ${SITE.title}`}>
<Layout title={`${t.pages.searchTitle} | ${config.site.title}`}>
<Header />
<Main pageTitle="Search" pageDesc="Search any article ...">
<div id="pagefind-search" transition:persist data-backurl={backUrl}></div>
<Breadcrumb />
<Main pageTitle={t.pages.searchTitle} pageDesc={t.pages.searchDesc}>
<div
id="pagefind-search"
transition:persist
data-backurl={backUrl}
data-bundle-path={pagefindBundlePath}
>
</div>
</Main>
<Footer />
</Layout>
@@ -24,6 +52,9 @@ const backUrl = SITE.showBackButton ? `${Astro.url.pathname}` : "/";
if (!pageFindSearch) return;
const bundlePath = pageFindSearch?.dataset?.bundlePath;
if (!bundlePath) return;
const params = new URLSearchParams(window.location.search);
const onIdle = window.requestIdleCallback || (cb => setTimeout(cb, 1));
@@ -45,14 +76,16 @@ const backUrl = SITE.showBackButton ? `${Astro.url.pathname}` : "/";
// Init pagefind ui
const search = new PagefindUI({
element: "#pagefind-search",
bundlePath,
showImages: false,
showSubResults: true,
processTerm: function (term: string) {
params.set("q", term); // Update the `q` parameter in the URL
history.replaceState(history.state, "", "?" + params.toString()); // Push the new URL without reloading
const backUrl = pageFindSearch?.dataset?.backurl;
sessionStorage.setItem("backUrl", backUrl + "?" + params.toString());
const backUrl =
pageFindSearch?.dataset?.backurl ?? window.location.pathname;
sessionStorage.setItem("backUrl", `${backUrl}?${params.toString()}`);
return term;
},
+22 -15
View File
@@ -1,44 +1,51 @@
---
import { getCollection } from "astro:content";
import type { GetStaticPathsOptions } from "astro";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Main from "@/components/Main.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import Card from "@/components/Card.astro";
import Footer from "@/components/Footer.astro";
import Pagination from "@/components/Pagination.astro";
import getUniqueTags from "@/utils/getUniqueTags";
import getPostsByTag from "@/utils/getPostsByTag";
import { SITE } from "@/config";
import { getUniqueTags } from "@/utils/getUniqueTags";
import { getSortedPosts } from "@/utils/getSortedPosts";
import { slugifyAll } from "@/utils/slugify";
import { useTranslations } from "@/i18n";
import config from "@/config";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const posts = await getCollection("blog");
const posts = await getCollection("posts", ({ data }) => !data.draft);
const tags = getUniqueTags(posts);
return tags.flatMap(({ tag, tagName }) => {
const tagPosts = getPostsByTag(posts, tag);
const tagPosts = getSortedPosts(
posts.filter(({ data }) => slugifyAll(data.tags).includes(tag))
);
return paginate(tagPosts, {
params: { tag },
props: { tagName },
pageSize: SITE.postPerPage,
pageSize: config.posts.perPage,
});
});
}
const params = Astro.params;
const { tag } = params;
const { page, tagName } = Astro.props;
const locale = Astro.currentLocale ?? config.site.lang;
const t = useTranslations(locale);
---
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
<Layout title={`${t.pages.tagTitle}: ${tagName} | ${config.site.title}`}>
<Header />
<Breadcrumb />
<Main
pageTitle={[`Tag:`, `${tagName}`]}
titleTransition={tag}
pageDesc={`All the articles with the tag "${tagName}".`}
pageTitle={`${t.pages.tagTitle}: ${tagName}`}
pageDesc={`${t.pages.tagDesc} "${tagName}".`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{page.data.map(data => <Card {...data} />)}
</ul>
+16 -8
View File
@@ -1,24 +1,32 @@
---
import { getCollection } from "astro:content";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Tag from "@/components/Tag.astro";
import Header from "@/components/Header.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import Main from "@/components/Main.astro";
import Footer from "@/components/Footer.astro";
import getUniqueTags from "@/utils/getUniqueTags";
import { SITE } from "@/config";
import Tag from "@/components/Tag.astro";
import { getUniqueTags } from "@/utils/getUniqueTags";
import { useTranslations } from "@/i18n";
import config from "@/config";
const posts = await getCollection("blog");
const posts = await getCollection("posts", ({ data }) => !data.draft);
const tags = getUniqueTags(posts);
let tags = getUniqueTags(posts);
const locale = Astro.currentLocale ?? config.site.lang;
const t = useTranslations(locale);
---
<Layout title={`Tags | ${SITE.title}`}>
<Layout title={`${t.pages.tagsTitle} | ${config.site.title}`}>
<Header />
<Main pageTitle="Tags" pageDesc="All the tags used in posts.">
<Breadcrumb />
<Main pageTitle={t.pages.tagsTitle} pageDesc={t.pages.tagsDesc}>
<ul class="flex flex-wrap gap-6">
{tags.map(({ tag, tagName }) => <Tag {tag} {tagName} />)}
</ul>
</Main>
<Footer />
</Layout>