mirror of
https://github.com/ChenQihan666/MyBlog-Next.git
synced 2026-08-14 07:33:07 +08:00
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:
@@ -1,24 +0,0 @@
|
||||
---
|
||||
import type { MarkdownLayoutProps } from "astro";
|
||||
import Header from "@/components/Header.astro";
|
||||
import Footer from "@/components/Footer.astro";
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro";
|
||||
import Layout from "./Layout.astro";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
type Props = MarkdownLayoutProps<{ title: string }>;
|
||||
|
||||
const { frontmatter } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`${frontmatter.title} | ${SITE.title}`}>
|
||||
<Header />
|
||||
<Breadcrumb />
|
||||
<main id="main-content" class="app-layout">
|
||||
<section id="about" class="app-prose mb-28 max-w-app prose-img:border-0">
|
||||
<h1 class="text-2xl tracking-wider sm:text-3xl">{frontmatter.title}</h1>
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
+59
-110
@@ -1,176 +1,125 @@
|
||||
---
|
||||
import { Font } from "astro:assets";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
import { PUBLIC_GOOGLE_SITE_VERIFICATION } from "astro:env/client";
|
||||
import { SITE } from "@/config";
|
||||
import { getRelativeLocaleUrl } from "astro:i18n";
|
||||
import { resolveDefaultOgImagePath } from "@/utils/resolveDefaultOgImagePath";
|
||||
import { getAssetPath } from "@/utils/withBase";
|
||||
import config from "@/config";
|
||||
import "@/styles/global.css";
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
author?: string;
|
||||
profile?: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
canonicalURL?: string;
|
||||
pubDatetime?: Date;
|
||||
modDatetime?: Date | null;
|
||||
scrollSmooth?: boolean;
|
||||
};
|
||||
|
||||
const { site } = config;
|
||||
|
||||
const {
|
||||
title = SITE.title,
|
||||
author = SITE.author,
|
||||
profile = SITE.profile,
|
||||
description = SITE.desc,
|
||||
ogImage = SITE.ogImage ? `/${SITE.ogImage}` : "/og.png",
|
||||
canonicalURL = new URL(Astro.url.pathname, Astro.url),
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
scrollSmooth = false,
|
||||
title = site.title,
|
||||
description = site.description,
|
||||
ogImage = resolveDefaultOgImagePath(config),
|
||||
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
|
||||
} = Astro.props;
|
||||
|
||||
const socialImageURL = new URL(ogImage, Astro.url);
|
||||
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: `${title}`,
|
||||
image: `${socialImageURL}`,
|
||||
datePublished: `${pubDatetime?.toISOString()}`,
|
||||
...(modDatetime && { dateModified: modDatetime.toISOString() }),
|
||||
author: [
|
||||
{
|
||||
"@type": "Person",
|
||||
name: `${author}`,
|
||||
...(profile && { url: profile }),
|
||||
},
|
||||
],
|
||||
};
|
||||
const socialImageURL = new URL(ogImage, Astro.site ?? Astro.url);
|
||||
const rssHref = getRelativeLocaleUrl(
|
||||
Astro.currentLocale ?? config.site.lang,
|
||||
"rss.xml"
|
||||
);
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html
|
||||
dir={SITE.dir}
|
||||
lang=`${SITE.lang ?? "en"}`
|
||||
class={`${scrollSmooth && "scroll-smooth"}`}
|
||||
dir={site.dir ?? "ltr"}
|
||||
lang={Astro.currentLocale ?? site.lang}
|
||||
class="overflow-y-scroll scroll-smooth"
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href={getAssetPath("favicon.svg")} />
|
||||
<link rel="icon" href={getAssetPath("favicon.ico")} />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Load Font -->
|
||||
<!-- Font -->
|
||||
<Font
|
||||
cssVariable="--font-google-sans-code"
|
||||
preload={[{ subset: "latin", weight: 400, style: "normal" }]}
|
||||
/>
|
||||
|
||||
<!-- General Meta Tags -->
|
||||
<!-- Primary meta -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="author" content={author} />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<meta name="author" content={site.author} />
|
||||
<link rel="sitemap" href={getAssetPath("sitemap-index.xml")} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={site.title} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonicalURL} />
|
||||
<meta property="og:image" content={socialImageURL} />
|
||||
|
||||
<!-- Article Published/Modified time -->
|
||||
{
|
||||
pubDatetime && (
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={pubDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
modDatetime && (
|
||||
<meta
|
||||
property="article:modified_time"
|
||||
content={modDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Twitter -->
|
||||
<!-- Twitter / X -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={canonicalURL} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={socialImageURL} />
|
||||
|
||||
<!-- Google JSON-LD Structured data -->
|
||||
<script
|
||||
type="application/ld+json"
|
||||
is:inline
|
||||
set:html={JSON.stringify(structuredData)}
|
||||
/>
|
||||
|
||||
<!-- Enable RSS feed auto-discovery -->
|
||||
<!-- https://docs.astro.build/en/recipes/rss/#enabling-rss-feed-auto-discovery -->
|
||||
<!-- RSS autodiscovery -->
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={SITE.title}
|
||||
href={new URL("rss.xml", Astro.site)}
|
||||
title={site.title}
|
||||
href={new URL(rssHref, Astro.site)}
|
||||
/>
|
||||
|
||||
<!-- Filled at runtime by theme.ts to match the current background colour -->
|
||||
<meta name="theme-color" content="" />
|
||||
|
||||
<!-- Extra head content injected by child layouts (e.g. JSON-LD, article meta) -->
|
||||
<slot name="head" />
|
||||
|
||||
{
|
||||
// If PUBLIC_GOOGLE_SITE_VERIFICATION is set in the environment variable,
|
||||
// include google-site-verification tag in the heading
|
||||
// Learn more: https://support.google.com/webmasters/answer/9008080#meta_tag_verification&zippy=%2Chtml-tag
|
||||
PUBLIC_GOOGLE_SITE_VERIFICATION && (
|
||||
site.googleVerification && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={PUBLIC_GOOGLE_SITE_VERIFICATION}
|
||||
content={site.googleVerification}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Minimal inline script to prevent FOUC - sets theme immediately -->
|
||||
<!--
|
||||
Inline FOUC-prevention script: sets data-theme on <html> before
|
||||
the browser paints. Runs synchronously, no defer/async.
|
||||
-->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const initialColorScheme = ""; // "light" | "dark"
|
||||
const currentTheme = localStorage.getItem("theme");
|
||||
|
||||
function getPreferTheme() {
|
||||
if (currentTheme) return currentTheme;
|
||||
if (initialColorScheme) return initialColorScheme;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
const themeValue = getPreferTheme();
|
||||
|
||||
// Set theme immediately to prevent flash
|
||||
document.firstElementChild?.setAttribute("data-theme", themeValue);
|
||||
|
||||
// Export minimal API for external script
|
||||
window.theme = {
|
||||
themeValue: themeValue,
|
||||
getTheme: () => window.theme.themeValue,
|
||||
setTheme: val => {
|
||||
window.theme.themeValue = val;
|
||||
},
|
||||
};
|
||||
const stored = localStorage.getItem("theme");
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
const theme = stored ?? (prefersDark ? "dark" : "light");
|
||||
document.firstElementChild?.setAttribute("data-theme", theme);
|
||||
// Expose value so theme.ts can skip re-detection.
|
||||
window.__theme = { value: theme };
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
||||
<!-- Load full theme logic -->
|
||||
<script src="../scripts/theme.ts"></script>
|
||||
<ClientRouter />
|
||||
</head>
|
||||
<body
|
||||
class="bg-background font-app text-foreground selection:bg-accent/75 selection:text-accent-foreground flex min-h-svh flex-col"
|
||||
>
|
||||
<slot />
|
||||
<script>
|
||||
import "@/scripts/theme";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
type StringTitle = { pageTitle: string };
|
||||
type ArrayTitle = { pageTitle: [string, string]; titleTransition: string };
|
||||
|
||||
type Props = (StringTitle | ArrayTitle) & { pageDesc?: string };
|
||||
|
||||
const { props } = Astro;
|
||||
|
||||
const backUrl = SITE.showBackButton ? Astro.url.pathname : "/";
|
||||
---
|
||||
|
||||
<Breadcrumb />
|
||||
<main data-backUrl={backUrl} id="main-content" class="app-layout pb-4">
|
||||
{
|
||||
"titleTransition" in props ? (
|
||||
<h1 class="text-2xl font-semibold sm:text-3xl">
|
||||
{props.pageTitle[0]}
|
||||
<span transition:name={props.titleTransition}>
|
||||
{props.pageTitle[1]}
|
||||
</span>
|
||||
</h1>
|
||||
) : (
|
||||
<h1 class="text-2xl font-semibold sm:text-3xl">{props.pageTitle}</h1>
|
||||
)
|
||||
}
|
||||
<p class="mt-2 mb-6 italic">{props.pageDesc}</p>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
const mainContent: HTMLElement | null =
|
||||
document.querySelector("#main-content");
|
||||
const backUrl = mainContent?.dataset?.backurl;
|
||||
if (backUrl) {
|
||||
sessionStorage.setItem("backUrl", backUrl);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,285 +0,0 @@
|
||||
---
|
||||
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";
|
||||
|
||||
type 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={["app-layout pb-12", { "mt-8": !SITE.showBackButton }]}
|
||||
data-pagefind-body
|
||||
>
|
||||
<h1
|
||||
transition:name={slugifyStr(title.replaceAll(".", "-"))}
|
||||
class="inline-block text-2xl font-bold text-accent 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={[
|
||||
"max-sm:hidden",
|
||||
{ hidden: !SITE.editPost.enabled || hideEditPost },
|
||||
]}>|</span
|
||||
>
|
||||
<EditPost {hideEditPost} {post} class="max-sm:hidden" />
|
||||
</div>
|
||||
<article
|
||||
id="article"
|
||||
class="app-prose mt-8 w-full 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 flex flex-wrap gap-4 sm:my-8">
|
||||
{tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} size="sm" />)}
|
||||
</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 rtl:rotate-180" />
|
||||
<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-end 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 rtl:rotate-180" />
|
||||
</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 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();
|
||||
|
||||
/** 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";
|
||||
|
||||
// Check if --file-name-offset custom property exists
|
||||
const computedStyle = getComputedStyle(codeBlock);
|
||||
const hasFileNameOffset =
|
||||
computedStyle.getPropertyValue("--file-name-offset").trim() !== "";
|
||||
|
||||
// Determine the top positioning class
|
||||
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);
|
||||
|
||||
// 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>
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
import Layout from "./Layout.astro";
|
||||
import config from "@/config";
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
canonicalURL?: string;
|
||||
pubDatetime?: Date;
|
||||
modDatetime?: Date | null;
|
||||
};
|
||||
|
||||
const { site } = config;
|
||||
|
||||
const { title, description, ogImage, canonicalURL, pubDatetime, modDatetime } =
|
||||
Astro.props;
|
||||
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: title ?? site.title,
|
||||
image: ogImage,
|
||||
...(pubDatetime && { datePublished: pubDatetime.toISOString() }),
|
||||
...(modDatetime && { dateModified: modDatetime.toISOString() }),
|
||||
author: [
|
||||
{
|
||||
"@type": "Person",
|
||||
name: site.author,
|
||||
...(site.profile && { url: site.profile }),
|
||||
},
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<Layout {title} {description} {ogImage} {canonicalURL}>
|
||||
<Fragment slot="head">
|
||||
<!-- Override og:type for article pages -->
|
||||
<meta property="og:type" content="article" />
|
||||
|
||||
{
|
||||
pubDatetime && (
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={pubDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
modDatetime && (
|
||||
<meta
|
||||
property="article:modified_time"
|
||||
content={modDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<script
|
||||
type="application/ld+json"
|
||||
is:inline
|
||||
set:html={JSON.stringify(structuredData)}
|
||||
/>
|
||||
</Fragment>
|
||||
|
||||
<slot />
|
||||
</Layout>
|
||||
Reference in New Issue
Block a user