Initial commit from Astro

This commit is contained in:
houston[bot] 2025-05-28 00:15:39 +02:00 committed by Gu://em_
commit 3dfd6a646d
123 changed files with 11791 additions and 0 deletions

30
src/pages/404.astro Normal file
View 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
View 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).

View 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
View 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
View 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" },
});

View 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>

View 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} />

View 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
View 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
View 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
View 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>

View 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>

View 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>