Initial commit from Astro
This commit is contained in:
commit
3dfd6a646d
123 changed files with 11791 additions and 0 deletions
30
src/pages/404.astro
Normal file
30
src/pages/404.astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
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";
|
||||
---
|
||||
|
||||
<Layout title={`404 Not Found | ${SITE.title}`}>
|
||||
<Header />
|
||||
|
||||
<main
|
||||
id="main-content"
|
||||
class="mx-auto flex max-w-3xl 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>
|
||||
<LinkButton
|
||||
href="/"
|
||||
class="my-6 text-lg underline decoration-dashed underline-offset-8"
|
||||
>
|
||||
Go back home
|
||||
</LinkButton>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
36
src/pages/about.md
Normal file
36
src/pages/about.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
layout: ../layouts/AboutLayout.astro
|
||||
title: "About"
|
||||
---
|
||||
|
||||
AstroPaper is a minimal, responsive and SEO-friendly Astro blog theme. I designed and crafted this based on [my personal blog](https://satnaing.dev/blog).
|
||||
|
||||
This theme is aimed to be accessible out of the box. Light and dark mode are supported by
|
||||
default and additional color schemes can also be configured.
|
||||
|
||||
This theme is self-documented \_ which means articles/posts in this theme can also be considered as documentations. So, see the documentation for more info.
|
||||
|
||||
<div>
|
||||
<img src="/dev.svg" class="sm:w-1/2 mx-auto" alt="coding dev illustration">
|
||||
</div>
|
||||
|
||||
## Tech Stack
|
||||
|
||||
This theme is written in vanilla JavaScript (+ TypeScript for type checking) and a little bit of ReactJS for some interactions. TailwindCSS is used for styling; and Markdown is used for blog contents.
|
||||
|
||||
## Features
|
||||
|
||||
Here are certain features of this site.
|
||||
|
||||
- fully responsive and accessible
|
||||
- SEO-friendly
|
||||
- light & dark mode
|
||||
- fuzzy search
|
||||
- super fast performance
|
||||
- draft posts
|
||||
- pagination
|
||||
- sitemap & rss feed
|
||||
- highly customizable
|
||||
|
||||
If you like this theme, you can star/contribute to the [repo](https://github.com/satnaing/astro-paper).
|
||||
Or you can even give any feedback via my [email](mailto:contact@satnaing.dev).
|
||||
83
src/pages/archives/index.astro
Normal file
83
src/pages/archives/index.astro
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
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 Card from "@/components/Card.astro";
|
||||
import getPostsByGroupCondition from "@/utils/getPostsByGroupCondition";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
// Redirect to 404 page if `showArchives` config is false
|
||||
if (!SITE.showArchives) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
---
|
||||
|
||||
<Layout title={`Archives | ${SITE.title}`}>
|
||||
<Header />
|
||||
<Main pageTitle="Archives" pageDesc="All the articles I've archived.">
|
||||
{
|
||||
Object.entries(
|
||||
getPostsByGroupCondition(posts, post =>
|
||||
post.data.pubDatetime.getFullYear()
|
||||
)
|
||||
)
|
||||
.sort(([yearA], [yearB]) => Number(yearB) - Number(yearA))
|
||||
.map(([year, yearGroup]) => (
|
||||
<div>
|
||||
<span class="text-2xl font-bold">{year}</span>
|
||||
<sup class="text-sm">{yearGroup.length}</sup>
|
||||
{Object.entries(
|
||||
getPostsByGroupCondition(
|
||||
yearGroup,
|
||||
post => post.data.pubDatetime.getMonth() + 1
|
||||
)
|
||||
)
|
||||
.sort(([monthA], [monthB]) => Number(monthB) - Number(monthA))
|
||||
.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>
|
||||
</div>
|
||||
<ul>
|
||||
{monthGroup
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.floor(
|
||||
new Date(b.data.pubDatetime).getTime() / 1000
|
||||
) -
|
||||
Math.floor(
|
||||
new Date(a.data.pubDatetime).getTime() / 1000
|
||||
)
|
||||
)
|
||||
.map(data => (
|
||||
<Card {...data} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
121
src/pages/index.astro
Normal file
121
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
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 Hr from "@/components/Hr.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";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
|
||||
const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Header />
|
||||
<main id="main-content" data-layout="index">
|
||||
<section id="hero" class="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"
|
||||
class="inline-block"
|
||||
aria-label="rss feed"
|
||||
title="RSS Feed"
|
||||
>
|
||||
<IconRss
|
||||
width={20}
|
||||
height={20}
|
||||
class="scale-125 stroke-accent stroke-3"
|
||||
/>
|
||||
<span class="sr-only">RSS Feed</span>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
AstroPaper is a minimal, responsive, accessible and SEO-friendly Astro
|
||||
blog theme. This theme follows best practices and provides accessibility
|
||||
out of the box. Light and dark mode are supported by default. Moreover,
|
||||
additional color schemes can also be configured.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
Read the blog posts or check
|
||||
<LinkButton
|
||||
class="underline decoration-dashed underline-offset-4 hover:text-accent"
|
||||
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 && (
|
||||
<div class="mt-4 flex flex-col sm:flex-row sm:items-center">
|
||||
<div class="mr-2 mb-1 whitespace-nowrap sm:mb-0">Social Links:</div>
|
||||
<Socials />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<Hr />
|
||||
|
||||
{
|
||||
featuredPosts.length > 0 && (
|
||||
<>
|
||||
<section id="featured" class="pt-12 pb-6">
|
||||
<h2 class="text-2xl font-semibold tracking-wide">Featured</h2>
|
||||
<ul>
|
||||
{featuredPosts.map(data => (
|
||||
<Card variant="h3" {...data} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
{recentPosts.length > 0 && <Hr />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
recentPosts.length > 0 && (
|
||||
<section id="recent-posts" class="pt-12 pb-6">
|
||||
<h2 class="text-2xl font-semibold tracking-wide">Recent Posts</h2>
|
||||
<ul>
|
||||
{recentPosts.map(
|
||||
(data, index) =>
|
||||
index < SITE.postPerIndex && <Card variant="h3" {...data} />
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="my-8 text-center">
|
||||
<LinkButton href="/posts/">
|
||||
All Posts
|
||||
<IconArrowRight class="inline-block" />
|
||||
</LinkButton>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
const indexLayout = (document.querySelector("#main-content") as HTMLElement)
|
||||
?.dataset?.layout;
|
||||
if (indexLayout) {
|
||||
sessionStorage.setItem("backUrl", "/");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
7
src/pages/og.png.ts
Normal file
7
src/pages/og.png.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { APIRoute } from "astro";
|
||||
import { generateOgImageForSite } from "@/utils/generateOgImages";
|
||||
|
||||
export const GET: APIRoute = async () =>
|
||||
new Response(await generateOgImageForSite(), {
|
||||
headers: { "Content-Type": "image/png" },
|
||||
});
|
||||
32
src/pages/posts/[...page].astro
Normal file
32
src/pages/posts/[...page].astro
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
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 Card from "@/components/Card.astro";
|
||||
import Pagination from "@/components/Pagination.astro";
|
||||
import getSortedPosts from "@/utils/getSortedPosts";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
export const getStaticPaths = (async ({ paginate }) => {
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
return paginate(getSortedPosts(posts), { pageSize: SITE.postPerPage });
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`Posts | ${SITE.title}`}>
|
||||
<Header />
|
||||
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
|
||||
<ul>
|
||||
{page.data.map(data => <Card {...data} />)}
|
||||
</ul>
|
||||
</Main>
|
||||
|
||||
<Pagination {page} />
|
||||
|
||||
<Footer noMarginTop={page.lastPage > 1} />
|
||||
</Layout>
|
||||
27
src/pages/posts/[...slug]/index.astro
Normal file
27
src/pages/posts/[...slug]/index.astro
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import PostDetails from "@/layouts/PostDetails.astro";
|
||||
import getSortedPosts from "@/utils/getSortedPosts";
|
||||
import { getPath } from "@/utils/getPath";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
}
|
||||
|
||||
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 },
|
||||
}));
|
||||
|
||||
return postResult;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
---
|
||||
|
||||
<PostDetails post={post} posts={sortedPosts} />
|
||||
36
src/pages/posts/[...slug]/index.png.ts
Normal file
36
src/pages/posts/[...slug]/index.png.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
if (!SITE.dynamicOgImage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const posts = await getCollection("blog").then(p =>
|
||||
p.filter(({ data }) => !data.draft && !data.ogImage)
|
||||
);
|
||||
|
||||
return posts.map(post => ({
|
||||
params: { slug: getPath(post.id, post.filePath, false) },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ props }) => {
|
||||
if (!SITE.dynamicOgImage) {
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
statusText: "Not found",
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
await generateOgImageForPost(props as CollectionEntry<"blog">),
|
||||
{
|
||||
headers: { "Content-Type": "image/png" },
|
||||
}
|
||||
);
|
||||
};
|
||||
13
src/pages/robots.txt.ts
Normal file
13
src/pages/robots.txt.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { APIRoute } from "astro";
|
||||
|
||||
const getRobotsTxt = (sitemapURL: URL) => `
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${sitemapURL.href}
|
||||
`;
|
||||
|
||||
export const GET: APIRoute = ({ site }) => {
|
||||
const sitemapURL = new URL("sitemap-index.xml", site);
|
||||
return new Response(getRobotsTxt(sitemapURL));
|
||||
};
|
||||
21
src/pages/rss.xml.ts
Normal file
21
src/pages/rss.xml.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
import { getPath } from "@/utils/getPath";
|
||||
import getSortedPosts from "@/utils/getSortedPosts";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
export async function GET() {
|
||||
const posts = await getCollection("blog");
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
return rss({
|
||||
title: SITE.title,
|
||||
description: SITE.desc,
|
||||
site: SITE.website,
|
||||
items: sortedPosts.map(({ data, id, filePath }) => ({
|
||||
link: getPath(id, filePath),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
pubDate: new Date(data.modDatetime ?? data.pubDatetime),
|
||||
})),
|
||||
});
|
||||
}
|
||||
139
src/pages/search.astro
Normal file
139
src/pages/search.astro
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
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 Footer from "@/components/Footer.astro";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
const backUrl = SITE.showBackButton ? `${Astro.url.pathname}` : "/";
|
||||
---
|
||||
|
||||
<Layout title={`Search | ${SITE.title}`}>
|
||||
<Header />
|
||||
<Main pageTitle="Search" pageDesc="Search any article ...">
|
||||
<div id="pagefind-search" transition:persist data-backurl={backUrl}></div>
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
function initSearch() {
|
||||
const pageFindSearch: HTMLElement | null =
|
||||
document.querySelector("#pagefind-search");
|
||||
|
||||
if (!pageFindSearch) return;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const onIdle = window.requestIdleCallback || (cb => setTimeout(cb, 1));
|
||||
|
||||
onIdle(async () => {
|
||||
// @ts-expect-error — Missing types for @pagefind/default-ui package.
|
||||
const { PagefindUI } = await import("@pagefind/default-ui");
|
||||
|
||||
// Display warning inn dev mode
|
||||
if (import.meta.env.DEV) {
|
||||
pageFindSearch.innerHTML = `
|
||||
<div class="bg-muted/75 rounded p-4 space-y-4 mb-4">
|
||||
<p><strong>DEV mode Warning! </strong>You need to build the project at least once to see the search results during development.</p>
|
||||
<code class="block bg-black text-white px-2 py-1 rounded">pnpm run build</code>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Init pagefind ui
|
||||
const search = new PagefindUI({
|
||||
element: "#pagefind-search",
|
||||
showSubResults: true,
|
||||
showImages: false,
|
||||
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());
|
||||
|
||||
return term;
|
||||
},
|
||||
});
|
||||
|
||||
// If search param exists (eg: search?q=astro), trigger search
|
||||
const query = params.get("q");
|
||||
if (query) {
|
||||
search.triggerSearch(query);
|
||||
}
|
||||
|
||||
// Reset search param if search input is cleared
|
||||
const searchInput = document.querySelector(".pagefind-ui__search-input");
|
||||
const clearButton = document.querySelector(".pagefind-ui__search-clear");
|
||||
searchInput?.addEventListener("input", resetSearchParam);
|
||||
clearButton?.addEventListener("click", resetSearchParam);
|
||||
|
||||
function resetSearchParam(e: Event) {
|
||||
if ((e.target as HTMLInputElement)?.value.trim() === "") {
|
||||
history.replaceState(history.state, "", window.location.pathname);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("astro:after-swap", () => {
|
||||
const pagefindSearch = document.querySelector("#pagefind-search");
|
||||
|
||||
// if pagefind search form already exists, don't initialize search component
|
||||
if (pagefindSearch && pagefindSearch.querySelector("form")) return;
|
||||
|
||||
initSearch();
|
||||
});
|
||||
initSearch();
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
#pagefind-search {
|
||||
--pagefind-ui-font: var(--font-mono);
|
||||
--pagefind-ui-text: var(--foreground);
|
||||
--pagefind-ui-background: var(--background);
|
||||
--pagefind-ui-border: var(--border);
|
||||
--pagefind-ui-primary: var(--accent);
|
||||
--pagefind-ui-tag: var(--background);
|
||||
--pagefind-ui-border-radius: 0.375rem;
|
||||
--pagefind-ui-border-width: 1px;
|
||||
--pagefind-ui-image-border-radius: 8px;
|
||||
--pagefind-ui-image-box-ratio: 3 / 2;
|
||||
|
||||
form::before {
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
input {
|
||||
font-weight: 400;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
input:focus-visible {
|
||||
outline: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.pagefind-ui__result-title a {
|
||||
color: var(--accent);
|
||||
outline-offset: 1px;
|
||||
outline-color: var(--accent);
|
||||
}
|
||||
|
||||
.pagefind-ui__result-title a:focus-visible,
|
||||
.pagefind-ui__search-clear:focus-visible {
|
||||
text-decoration-line: none;
|
||||
outline-width: 2px;
|
||||
outline-style: dashed;
|
||||
}
|
||||
|
||||
.pagefind-ui__result:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.pagefind-ui__result-nested .pagefind-ui__result-link:before {
|
||||
font-family: system-ui;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
src/pages/tags/[tag]/[...page].astro
Normal file
50
src/pages/tags/[tag]/[...page].astro
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
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 Card from "@/components/Card.astro";
|
||||
import Pagination from "@/components/Pagination.astro";
|
||||
import getUniqueTags from "@/utils/getUniqueTags";
|
||||
import getPostsByTag from "@/utils/getPostsByTag";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
||||
const posts = await getCollection("blog");
|
||||
const tags = getUniqueTags(posts);
|
||||
|
||||
return tags.flatMap(({ tag, tagName }) => {
|
||||
const tagPosts = getPostsByTag(posts, tag);
|
||||
|
||||
return paginate(tagPosts, {
|
||||
params: { tag },
|
||||
props: { tagName },
|
||||
pageSize: SITE.postPerPage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const params = Astro.params;
|
||||
const { tag } = params;
|
||||
const { page, tagName } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
|
||||
<Header />
|
||||
<Main
|
||||
pageTitle={[`Tag:`, `${tagName}`]}
|
||||
titleTransition={tag}
|
||||
pageDesc={`All the articles with the tag "${tagName}".`}
|
||||
>
|
||||
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
|
||||
<ul>
|
||||
{page.data.map(data => <Card {...data} />)}
|
||||
</ul>
|
||||
</Main>
|
||||
|
||||
<Pagination {page} />
|
||||
|
||||
<Footer noMarginTop={page.lastPage > 1} />
|
||||
</Layout>
|
||||
24
src/pages/tags/index.astro
Normal file
24
src/pages/tags/index.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
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 Footer from "@/components/Footer.astro";
|
||||
import getUniqueTags from "@/utils/getUniqueTags";
|
||||
import { SITE } from "@/config";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
let tags = getUniqueTags(posts);
|
||||
---
|
||||
|
||||
<Layout title={`Tags | ${SITE.title}`}>
|
||||
<Header />
|
||||
<Main pageTitle="Tags" pageDesc="All the tags used in posts.">
|
||||
<ul>
|
||||
{tags.map(({ tag, tagName }) => <Tag {tag} {tagName} size="lg" />)}
|
||||
</ul>
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
Loading…
Add table
Add a link
Reference in a new issue