Initial commit from Astro

This commit is contained in:
houston[bot]
2026-01-29 10:17:40 +01:00
committed by vorpax
commit c29b05bff3
123 changed files with 12279 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="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>
<LinkButton
href="/"
class="my-6 text-lg underline decoration-dashed underline-offset-8"
>
Go back home
</LinkButton>
</div>
</main>
<Footer />
</Layout>

37
src/pages/about.md Normal file
View File

@@ -0,0 +1,37 @@
---
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! 🙏🏼

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 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" class="app-layout">
<section id="hero" class:list={["pt-8 pb-6", "border-b border-border"]}>
<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 rtl:-rotate-90"
/>
<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 max-sm:flex-col sm:items-center">
<div class="me-2 mb-1 whitespace-nowrap sm:mb-0">Social Links:</div>
<Socials />
</div>
)
}
</section>
{
featuredPosts.length > 0 && (
<section
id="featured"
class:list={[
"pt-12 pb-6",
{ "border-b border-border": recentPosts.length > 0 },
]}
>
<h2 class="text-2xl font-semibold tracking-wide">Featured</h2>
<ul>
{featuredPosts.map(data => (
<Card variant="h3" {...data} />
))}
</ul>
</section>
)
}
{
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 rtl:-rotate-180" />
</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>

9
src/pages/og.png.ts Normal file
View File

@@ -0,0 +1,9 @@
import type { APIRoute } from "astro";
import { generateOgImageForSite } from "@/utils/generateOgImages";
export const GET: APIRoute = async () => {
const buffer = await generateOgImageForSite();
return new Response(new Uint8Array(buffer), {
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";
type 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,34 @@
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",
});
}
const buffer = await generateOgImageForPost(props as CollectionEntry<"blog">);
return new Response(new Uint8Array(buffer), {
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),
})),
});
}

141
src/pages/search.astro Normal file
View File

@@ -0,0 +1,141 @@
---
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",
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());
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-app);
--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);
text-decoration-style: dashed;
text-underline-offset: 4px;
}
.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 class="flex flex-wrap gap-6">
{tags.map(({ tag, tagName }) => <Tag {tag} {tagName} />)}
</ul>
</Main>
<Footer />
</Layout>