Initial commit: astro-micro scaffold

Amp-Thread-ID: https://ampcode.com/threads/T-019c7839-0936-729b-aa87-92b35d3d68fe
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Neeldhara Misra 2026-02-20 04:57:48 +05:30
commit 93917727fb
108 changed files with 12180 additions and 0 deletions

View file

@ -0,0 +1,42 @@
---
import type { CollectionEntry } from "astro:content";
type Props = {
entry: CollectionEntry<"blog"> | CollectionEntry<"projects">;
};
const { entry } = Astro.props as {
entry: CollectionEntry<"blog"> | CollectionEntry<"projects">;
};
---
<a
href={`/${entry.collection}/${entry.id}`}
class="not-prose group relative flex flex-nowrap rounded-lg border border-black/15 px-4 py-3 pr-10 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<div class="flex flex-1 flex-col truncate">
<div class="font-semibold">
{entry.data.title}
</div>
<div class="text-sm">
{entry.data.description}
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute right-2 top-1/2 size-5 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 19 12 12 19"
class="-translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
></polyline>
</svg>
</a>

View file

@ -0,0 +1,33 @@
---
type Props = {
href: string;
};
const { href } = Astro.props;
---
<a
href={href}
class="not-prose group relative flex w-fit flex-nowrap rounded-sm border border-black/15 py-1.5 pl-7 pr-3 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute left-2 top-1/2 size-4 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
></polyline>
</svg>
<div class="text-sm">
<slot />
</div>
</a>

View file

@ -0,0 +1,40 @@
---
---
<script>
if (typeof window !== 'undefined') {
window.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('back-to-top');
if (btn) {
btn.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
});
}
</script>
<button
id="back-to-top"
class="group relative flex w-fit flex-nowrap rounded-sm border border-black/15 py-1.5 pl-8 pr-3 transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute left-2 top-1/2 size-4 -translate-y-1/2 rotate-90 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
></line>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
></polyline>
</svg>
<div class="text-sm">Back to top</div>
</button>

View file

@ -0,0 +1,30 @@
---
interface Props {
type?: "default" | "info" | "warning" | "error";
}
const { type = "default" } = Astro.props;
let emoji = "💡";
if (type === "info") {
emoji = "";
} else if (type === "warning") {
emoji = "⚠️";
} else if (type === "error") {
emoji = "🚨";
}
const baseClasses = "relative my-4 flex rounded border p-3";
const typeClasses = {
default:
"border-orange-800 bg-orange-100 text-orange-950 dark:border-orange-200/20 dark:bg-orange-950/20 dark:text-orange-200",
info: "border-blue-800 bg-blue-100 text-blue-950 dark:border-blue-200/20 dark:bg-blue-950/20 dark:text-blue-200",
warning:
"border-yellow-800 bg-yellow-100 text-yellow-950 dark:border-yellow-200/20 dark:bg-yellow-950/20 dark:text-yellow-200",
error:
"border-red-800 bg-red-100 text-red-950 dark:border-red-200/20 dark:bg-red-950/20 dark:text-red-200",
};
---
<div class:list={["not-prose", baseClasses, typeClasses[type]]}>
<span class="pointer-events-none pr-3 text-xl select-none">{emoji}</span>
<slot />
</div>

View file

@ -0,0 +1,5 @@
---
---
<div class="mx-auto max-w-(--breakpoint-sm) px-3"><slot /></div>

View file

@ -0,0 +1,90 @@
---
import Container from "@components/Container.astro";
import { SITE } from "@consts";
import BackToTop from "@components/BackToTop.astro";
---
<footer class="animate">
<Container>
<div class="relative">
<div class="absolute -top-12 right-0">
<BackToTop />
</div>
</div>
<div class="flex items-center justify-between">
<div>&copy; {new Date().getFullYear()} • {SITE.TITLE} 🔬</div>
<div class="flex flex-wrap items-center gap-1.5">
<button
id="light-theme-button"
aria-label="Light theme"
class="group flex size-9 items-center justify-center rounded-sm border border-black/15 hover:bg-black/5 focus-visible:bg-black/5 dark:border-white/20 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-colors duration-300 ease-in-out group-hover:animate-pulse group-hover:stroke-black group-focus-visible:animate-pulse group-focus-visible:stroke-black dark:group-hover:stroke-white dark:group-focus-visible:stroke-white"
>
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<button
id="dark-theme-button"
aria-label="Dark theme"
class="group flex size-9 items-center justify-center rounded-sm border border-black/15 hover:bg-black/5 focus-visible:bg-black/5 dark:border-white/20 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-colors duration-300 ease-in-out group-hover:animate-pulse group-hover:stroke-black group-focus-visible:animate-pulse group-focus-visible:stroke-black dark:group-hover:stroke-white dark:group-focus-visible:stroke-white"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<button
id="system-theme-button"
aria-label="System theme"
class="group flex size-9 items-center justify-center rounded-sm border border-black/15 hover:bg-black/5 focus-visible:bg-black/5 dark:border-white/20 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-colors duration-300 ease-in-out group-hover:animate-pulse group-hover:stroke-black group-focus-visible:animate-pulse group-focus-visible:stroke-black dark:group-hover:stroke-white dark:group-focus-visible:stroke-white"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
</button>
</div>
</div>
</Container>
</footer>

View file

@ -0,0 +1,17 @@
---
interface Props {
date: Date;
}
const { date } = Astro.props;
---
<time datetime={date.toISOString()}>
{
date.toLocaleDateString("en-US", {
month: "long",
day: "2-digit",
year: "numeric",
})
}
</time>

View file

@ -0,0 +1,20 @@
<div class="giscus"></div>
<script
is:inline
data-astro-rerun
src="https://giscus.app/client.js"
data-repo="trevortylerlee/astro-micro"
data-repo-id="R_kgDOL_6l9Q"
data-category="Announcements"
data-category-id="DIC_kwDOL_6l9c4Cfk55"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme="preferred_color_scheme"
data-lang="en"
data-loading="lazy"
crossorigin="anonymous"
async></script>

267
src/components/Head.astro Normal file
View file

@ -0,0 +1,267 @@
---
import "../styles/global.css";
import { ClientRouter } from "astro:transitions";
import "@fontsource/geist-sans/100.css";
import "@fontsource/geist-sans/200.css";
import "@fontsource/geist-sans/300.css";
import "@fontsource/geist-sans/400.css";
import "@fontsource/geist-sans/500.css";
import "@fontsource/geist-sans/600.css";
import "@fontsource/geist-sans/700.css";
import "@fontsource/geist-sans/800.css";
import "@fontsource/geist-sans/900.css";
import "@fontsource/geist-mono/100.css";
import "@fontsource/geist-mono/200.css";
import "@fontsource/geist-mono/300.css";
import "@fontsource/geist-mono/400.css";
import "@fontsource/geist-mono/500.css";
import "@fontsource/geist-mono/600.css";
import "@fontsource/geist-mono/700.css";
import "@fontsource/geist-mono/800.css";
import "@fontsource/geist-mono/900.css";
interface Props {
title: string;
description: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = "/astro-micro.jpg" } = Astro.props;
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔬</text></svg>"
/>
<meta name="generator" content={Astro.generator} />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<!-- PageFind -->
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script is:inline src="/pagefind/pagefind-ui.js"></script>
<ClientRouter />
<script is:inline>
function init() {
preloadTheme();
onScroll();
animate();
updateThemeButtons();
addCopyCodeButtons();
setGiscusTheme();
const backToTop = document.getElementById("back-to-top");
backToTop?.addEventListener("click", (event) => scrollToTop(event));
const backToPrev = document.getElementById("back-to-prev");
backToPrev?.addEventListener("click", () => window.history.back());
const lightThemeButton = document.getElementById("light-theme-button");
lightThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "light");
toggleTheme(false);
updateThemeButtons();
});
const darkThemeButton = document.getElementById("dark-theme-button");
darkThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "dark");
toggleTheme(true);
updateThemeButtons();
});
const systemThemeButton = document.getElementById("system-theme-button");
systemThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "system");
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
updateThemeButtons();
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
if (localStorage.theme === "system") {
toggleTheme(event.matches);
}
});
document.addEventListener("scroll", onScroll);
}
function updateThemeButtons() {
const theme = localStorage.getItem("theme");
const lightThemeButton = document.getElementById("light-theme-button");
const darkThemeButton = document.getElementById("dark-theme-button");
const systemThemeButton = document.getElementById("system-theme-button");
function removeActiveButtonTheme(button) {
button?.classList.remove("bg-black/5");
button?.classList.remove("dark:bg-white/5");
}
function addActiveButtonTheme(button) {
button?.classList.add("bg-black/5");
button?.classList.add("dark:bg-white/5");
}
removeActiveButtonTheme(lightThemeButton);
removeActiveButtonTheme(darkThemeButton);
removeActiveButtonTheme(systemThemeButton);
if (theme === "light") {
addActiveButtonTheme(lightThemeButton);
} else if (theme === "dark") {
addActiveButtonTheme(darkThemeButton);
} else {
addActiveButtonTheme(systemThemeButton);
}
}
function animate() {
const animateElements = document.querySelectorAll(".animate");
animateElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add("show");
}, index * 100);
});
}
function onScroll() {
if (window.scrollY > 0) {
document.documentElement.classList.add("scrolled");
} else {
document.documentElement.classList.remove("scrolled");
}
}
function scrollToTop(event) {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
function toggleTheme(dark) {
const css = document.createElement("style");
css.appendChild(
document.createTextNode(
`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}
`,
),
);
document.head.appendChild(css);
if (dark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
window.getComputedStyle(css).opacity;
document.head.removeChild(css);
setGiscusTheme();
}
function preloadTheme() {
const userTheme = localStorage.theme;
if (userTheme === "light" || userTheme === "dark") {
toggleTheme(userTheme === "dark");
} else {
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
}
}
function addCopyCodeButtons() {
let copyButtonLabel = "📋";
let codeBlocks = Array.from(document.querySelectorAll("pre"));
async function copyCode(codeBlock, copyButton) {
const codeText = codeBlock.innerText;
const buttonText = copyButton.innerText;
const textToCopy = codeText.replace(buttonText, "");
await navigator.clipboard.writeText(textToCopy);
copyButton.innerText = "✅";
setTimeout(() => {
copyButton.innerText = copyButtonLabel;
}, 2000);
}
for (let codeBlock of codeBlocks) {
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
const copyButton = document.createElement("button");
copyButton.innerText = copyButtonLabel;
copyButton.classList = "copy-code";
codeBlock.setAttribute("tabindex", "0");
codeBlock.appendChild(copyButton);
codeBlock.parentNode.insertBefore(wrapper, codeBlock);
wrapper.appendChild(codeBlock);
copyButton?.addEventListener("click", async () => {
await copyCode(codeBlock, copyButton);
});
}
}
const setGiscusTheme = () => {
const giscus = document.querySelector(".giscus-frame");
const isDark = document.documentElement.classList.contains("dark");
if (giscus) {
const url = new URL(giscus.src);
url.searchParams.set("theme", isDark ? "dark" : "light");
giscus.src = url.toString();
}
};
document.addEventListener("DOMContentLoaded", () => init());
document.addEventListener("astro:after-swap", () => init());
preloadTheme();
</script>

View file

@ -0,0 +1,46 @@
---
import Container from "@components/Container.astro";
import Link from "@components/Link.astro";
import { SITE } from "@consts";
---
<header transition:persist>
<Container>
<div class="flex flex-wrap justify-between gap-y-2">
<Link href="/" underline={false}>
<div class="font-semibold">
{SITE.TITLE}&nbsp;🔬
</div>
</Link>
<nav class="flex items-center gap-1 text-sm">
<Link href="/blog">blog</Link>
<span>
{`/`}
</span>
<Link href="/projects">projects</Link>
<span>
{`/`}
</span>
<button
id="magnifying-glass"
aria-label="Search"
class="flex items-center rounded-sm border border-black/15 bg-neutral-100 px-2 py-1 text-xs transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:bg-neutral-900 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
height="16"
stroke-linejoin="round"
viewBox="0 0 16 16"
width="16"
style="color: currentcolor;"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.5 7C3.5 5.067 5.067 3.5 7 3.5C8.933 3.5 10.5 5.067 10.5 7C10.5 7.88461 10.1718 8.69256 9.63058 9.30876L9.30876 9.63058C8.69256 10.1718 7.88461 10.5 7 10.5C5.067 10.5 3.5 8.933 3.5 7ZM9.96544 11.0261C9.13578 11.6382 8.11014 12 7 12C4.23858 12 2 9.76142 2 7C2 4.23858 4.23858 2 7 2C9.76142 2 12 4.23858 12 7C12 8.11014 11.6382 9.13578 11.0261 9.96544L14.0303 12.9697L14.5607 13.5L13.5 14.5607L12.9697 14.0303L9.96544 11.0261Z"
fill="currentColor"></path></svg
>
&nbsp;Search
</button>
</nav>
</div>
</Container>
</header>

31
src/components/Link.astro Normal file
View file

@ -0,0 +1,31 @@
---
import { cn } from "@lib/utils";
type Props = {
href: string;
external?: boolean;
underline?: boolean;
group?: boolean;
};
const {
href,
external,
underline = true,
group = false,
...rest
} = Astro.props;
---
<a
href={href}
target={external ? "_blank" : "_self"}
class={cn(
"inline-block decoration-black/30 dark:decoration-white/30 hover:decoration-black/50 focus-visible:decoration-black/50 dark:hover:decoration-white/50 dark:focus-visible:decoration-white/50 text-current hover:text-black focus-visible:text-black dark:hover:text-white dark:focus-visible:text-white transition-colors duration-300 ease-in-out",
underline && "underline underline-offset-[3px]",
group && "group"
)}
{...rest}
>
<slot />
</a>

View file

@ -0,0 +1,132 @@
---
import Search from "astro-pagefind/components/Search";
---
<aside data-pagefind-ignore>
<div
transition:persist
id="backdrop"
class="bg-[rgba(0, 0, 0, 0.5] invisible fixed left-0 top-0 z-50 flex h-screen w-full justify-center p-6 backdrop-blur-xs"
>
<div
id="pagefind-container"
class="m-0 flex h-fit max-h-[80%] w-full max-w-(--breakpoint-sm) flex-col overflow-auto rounded-sm border border-black/15 bg-neutral-100 p-2 px-4 py-3 shadow-lg dark:border-white/20 dark:bg-neutral-900"
>
<Search
id="search"
className="pagefind-ui"
uiOptions={{
showImages: false,
excerptLength: 15,
resetStyles: false,
}}
/>
<div class="mr-2 pb-1 pt-4 text-right text-xs dark:prose-invert">
Press <span class="prose text-xs dark:prose-invert"
><kbd class="">Esc</kbd></span
> or click anywhere to close
</div>
</div>
</div>
</aside>
<script is:inline>
const magnifyingGlass = document.getElementById("magnifying-glass");
const backdrop = document.getElementById("backdrop");
function openPagefind() {
const searchDiv = document.getElementById("search");
const search = searchDiv.querySelector("input");
setTimeout(() => {
search.focus();
}, 0);
backdrop?.classList.remove("invisible");
backdrop?.classList.add("visible");
}
function closePagefind() {
const search = document.getElementById("search");
search.value = "";
backdrop?.classList.remove("visible");
backdrop?.classList.add("invisible");
}
// open pagefind
magnifyingGlass?.addEventListener("click", () => {
openPagefind();
});
document.addEventListener("keydown", (e) => {
if (e.key === "/") {
e.preventDefault();
openPagefind();
} else if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
openPagefind();
}
});
// close pagefind
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closePagefind();
}
});
// close pagefind when searched result(link) clicked
document.addEventListener("click", (event) => {
if (event.target.classList.contains("pagefind-ui__result-link")) {
closePagefind();
}
});
backdrop?.addEventListener("click", (event) => {
if (!event.target.closest("#pagefind-container")) {
closePagefind();
}
});
// prevent form submission
const form = document.getElementById("form");
form?.addEventListener("submit", (event) => {
event.preventDefault();
});
</script>
<style is:global>
:root {
--pagefind-ui-scale: 0.75;
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: 3px;
--pagefind-ui-font: "Geist", sans-serif;
--pagefind-ui-primary: #3d3d3d;
--pagefind-ui-text: #3d3d3d;
--pagefind-ui-background: #ffffff;
--pagefind-ui-border: #d0d0d0;
--pagefind-ui-tag: #f5f5f5;
}
.dark {
--pagefind-ui-primary: #d4d4d4;
--pagefind-ui-text: #d4d4d4;
--pagefind-ui-background: #171717;
--pagefind-ui-border: #404040;
}
#search input {
font-weight: normal;
}
#search p {
font-weight: normal;
}
#search .pagefind-ui__result-title {
font-weight: 600;
}
#search .pagefind-ui__message {
padding: 0;
padding-bottom: 0.75rem;
}
</style>

View file

@ -0,0 +1,65 @@
---
const { prevPost, nextPost } = Astro.props;
---
<div class="grid grid-cols-2 gap-1.5 sm:gap-3">
{
prevPost?.id ? (
<a
href={`/blog/${prevPost?.id}`}
class="group relative flex flex-nowrap rounded-lg border border-black/15 px-4 py-3 pl-10 no-underline transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute left-2 top-1/2 size-5 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
/>
<polyline
points="12 5 5 12 12 19"
class="translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
/>
</svg>
<div class="flex items-center text-sm">{prevPost?.data.title}</div>
</a>
) : (
<div class="invisible" />
)
}
{
nextPost?.id ? (
<a
href={`/blog/${nextPost?.id}`}
class="group relative flex grow flex-row-reverse flex-nowrap rounded-lg border border-black/15 px-4 py-4 pr-10 no-underline transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute right-2 top-1/2 size-5 -translate-y-1/2 fill-none stroke-current stroke-2"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-hover:scale-x-100 group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
/>
<polyline
points="12 5 19 12 12 19"
class="-translate-x-1 transition-transform duration-300 ease-in-out group-hover:translate-x-0 group-focus-visible:translate-x-0"
/>
</svg>
<div class="flex items-center text-sm">{nextPost?.data.title}</div>
</a>
) : (
<div class="invisible" />
)
}
</div>

View file

@ -0,0 +1,43 @@
---
import TableOfContentsHeading from "./TableOfContentsHeading.astro";
// https://kld.dev/building-table-of-contents/
const { headings } = Astro.props;
const toc = buildToc(headings);
export interface Heading {
depth: number;
slug: string;
text: string;
}
function buildToc(headings: Heading[]) {
const toc: Heading[] = [];
const parentHeadings = new Map();
headings.forEach((h) => {
const heading = { ...h, subheadings: [] };
parentHeadings.set(heading.depth, heading);
if (heading.depth === 2) {
toc.push(heading);
} else {
parentHeadings.get(heading.depth - 1).subheadings.push(heading);
}
});
return toc;
}
---
<details
open
class="animate group rounded-lg border border-black/15 dark:border-white/20"
>
<summary
class="cursor-pointer rounded-t-lg px-3 py-1.5 font-medium transition-colors group-open:bg-black/5 hover:bg-black/5 dark:group-open:bg-white/5 hover:dark:bg-white/5"
>Table of Contents</summary
>
<nav>
<ul class="py-3">
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul>
</nav>
</details>

View file

@ -0,0 +1,23 @@
---
import type { Heading } from "./TableOfContents.astro";
import Link from "./Link.astro";
// https://kld.dev/building-table-of-contents/
const { heading } = Astro.props;
---
<li class="list-inside list-disc px-6 py-1.5 text-sm">
<Link href={"#" + heading.slug} underline>
{heading.text}
</Link>
{
heading.subheadings.length > 0 && (
<ul class="translate-x-3">
{heading.subheadings.map((subheading: Heading) => (
<Astro.self heading={subheading} />
))}
</ul>
)
}
</li>

View file

@ -0,0 +1,10 @@
---
import { getCollection } from "astro:content";
const allPosts = await getCollection("blog");
const allTags = allPosts.map((tag) => tag.data.tags).flat();
console.log(allTags);
---
<span> </span>

40
src/consts.ts Normal file
View file

@ -0,0 +1,40 @@
import type { Metadata, Site, Socials } from "@types";
export const SITE: Site = {
TITLE: "Micro",
DESCRIPTION: "Neeldhara's micro blog.",
EMAIL: "neeldhara.mishra@gmail.com",
NUM_POSTS_ON_HOMEPAGE: 5,
NUM_PROJECTS_ON_HOMEPAGE: 3,
};
export const HOME: Metadata = {
TITLE: "Home",
DESCRIPTION: "Astro Micro is an accessible theme for Astro.",
};
export const BLOG: Metadata = {
TITLE: "Blog",
DESCRIPTION: "A collection of articles on topics I am passionate about.",
};
export const PROJECTS: Metadata = {
TITLE: "Projects",
DESCRIPTION:
"A collection of my projects with links to repositories and live demos.",
};
export const SOCIALS: Socials = [
{
NAME: "X (formerly Twitter)",
HREF: "https://twitter.com/boogerbuttcheek",
},
{
NAME: "GitHub",
HREF: "https://github.com/trevortylerlee",
},
{
NAME: "Website",
HREF: "https://trevortylerlee.com",
},
];

27
src/content.config.ts Normal file
View file

@ -0,0 +1,27 @@
import { defineCollection, z } from "astro:content";
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
draft: z.boolean().optional(),
tags: z.array(z.string()).optional(),
}),
});
const projects = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: "./src/content/projects" }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
draft: z.boolean().optional(),
demoURL: z.string().optional(),
repoURL: z.string().optional(),
}),
});
export const collections = { blog, projects };

View file

@ -0,0 +1,112 @@
---
title: "Everything new in Astro Micro"
description: "Features, enhancements, and changes."
date: "2024-05-09"
---
import Callout from "@/components/Callout.astro";
---
## Pagefind search 🔎
[Pagefind](https://pagefind.app) is a search library for static websites. Micro uses [Sergey Shishkin's](https://github.com/shishkin) [astro-pagefind](https://github.com/shishkin/astro-pagefind) integration. This integration simplifies development and does not require any changes to the default build script.
Press <kbd>/</kbd> or <kbd>CTRL</kbd> + <kbd>K</kbd> to open the search dialog. For Mac users, <kbd>⌘</kbd> + <kbd>K</kbd> can also be used. To dismiss the search dialog, press <kbd>Esc</kbd> or click on an area outside the component.
### Build and develop
<Callout type="error">
The site **must** be built at least once so Pagefind can index the content.
</Callout>
```bash
# Pagefind must index the site to function
npm run build
```
When developing you can continue to use `npm run dev` and Pagefind will use the index from the last available build.
---
## Giscus comments 💬
[Giscus](https://giscus.app) leverages Github discussions to act as a comments system. To get Giscus working on your own website, see [here](/blog/01-getting-started#deploy-the-site).
---
## Callout component 🆕
<Callout>
Adipisicing et officia reprehenderit fugiat occaecat cupidatat exercitation
labore consequat ullamco nostrud non.
</Callout>
<Callout type="info">
Adipisicing et officia reprehenderit fugiat occaecat cupidatat exercitation
labore consequat ullamco nostrud non.
</Callout>
<Callout type="warning">
Adipisicing et officia reprehenderit fugiat occaecat cupidatat exercitation
labore consequat ullamco nostrud non.
</Callout>
<Callout type="error">
Adipisicing et officia reprehenderit fugiat occaecat cupidatat exercitation
labore consequat ullamco nostrud non.
</Callout>
---
## UI enhancements 🎨
- Elements are styled and animate on focus
- Increased contrast in light mode
- Active theme is indicated by theme buttons
- Separate syntax highlight themes for light and dark mode
- Code blocks have a copy button
- Add pagination to the bottom of blog posts
- Create 404 page
- Add ToC component to posts
---
## Other changes
- Change fonts to Geist Sans and Geist Mono
- Switch base color from "stone" to "neutral"
- Change formatted date to use "long" option for month
- Minor spacing changes throughout
- Remove "work" collection and components
- If desired, you can get the code from [Astro Nano](https://github.com/markhorn-dev/astro-nano)
- Slightly increased link decoration offset
- Slightly sped-up animations
- Reversed animation
- Ensure posts use an h1 tag for post titles
- Tweaked typography
---
## Issues ⚠️
### Active issues
No active issues!
### Closed issues
- Fixed by [blopker](https://github.com/blopker): [ToC links are obscured by Header when scrolled to](https://github.com/trevortylerlee/astro-micro/issues/4)
- Fixed by [blopker](https://github.com/blopker): [Pagefind requires a refresh to function when ViewTransitions is enabled](https://github.com/trevortylerlee/astro-micro/issues/7)
- Fixed by [arastoonet](https://github.com/arastoonet): [Fix typo in README](https://github.com/trevortylerlee/astro-micro/pull/19)
- Fixed by [cgranier](https://github.com/cgranier): [Pagination links advance by slug/folder](https://github.com/trevortylerlee/astro-micro/issues/26)
- Fixed by [cgranier](https://github.com/cgranier): [Hides Table of Contents when there are no headings](https://github.com/trevortylerlee/astro-micro/pull/30)
- Fixed: [Giscus does not appear when switching blogs via post navigation](https://github.com/trevortylerlee/astro-micro/issues/32)
- Fixed: [Geist font renders incorrectly on Windows](https://github.com/trevortylerlee/astro-micro/issues/33)
- Fixed: [Pagination fails in Cloudflare Pages](https://github.com/trevortylerlee/astro-micro/issues/39)
- Fixed by [luciancah](https://github.com/luciancah): [Prevent Pagefind from double-indexing results](https://github.com/trevortylerlee/astro-micro/issues/40)
- Fixed by [luciancah](https://github.com/luciancah): [Pagefind should close if a result is clicked](https://github.com/trevortylerlee/astro-micro/issues/43)
- Fixed: [Implement tags](https://github.com/trevortylerlee/astro-micro/issues/70)
- Fixed by [anaxite](https://github.com/anaxite): [Update Astro to 5.0](https://github.com/trevortylerlee/astro-micro/issues/73)
- Fixed by [MoyaF](https://github.com/MoyaF): [Table of Contents links not working](https://github.com/trevortylerlee/astro-micro/issues/75)
- Fixed by [antoniovalentini](https://github.com/antoniovalentini): [Possible bugs due to outdated Astro version 5.0.3](https://github.com/trevortylerlee/astro-micro/issues/78)

View file

@ -0,0 +1,182 @@
---
title: "Getting started"
description: "Hit the ground running."
date: "2024-03-22"
tags:
- tutorial
---
---
## Install astro-micro
Clone the [Astro Micro repository](https://github.com/trevortylerlee/astro-micro.git).
```sh
git clone https://github.com/trevortylerlee/astro-micro.git my-astro-micro
```
```sh
cd my-astro-micro
```
```sh
npm i
```
```sh
npm run build
```
```sh
npm run dev
```
## Customize the website metadata
To change the website metadata, edit `src/consts.ts`.
```ts
// src/consts.ts
export const SITE: Site = {
NAME: "Astro Micro",
DESCRIPTION: "Astro Micro is an accessible theme for Astro.",
EMAIL: "trevortylerlee@gmail.com",
NUM_POSTS_ON_HOMEPAGE: 3,
NUM_PROJECTS_ON_HOMEPAGE: 3,
};
```
| Field | Req | Description |
| :----------- | :-- | :--------------------------------------------------- |
| TITLE | Yes | Displayed in header and footer. Used in SEO and RSS. |
| DESCRIPTION | Yes | Used in SEO and RSS. |
| EMAIL | Yes | Displayed in contact section. |
| NUM_POSTS | Yes | Limit number of posts on home page. |
| NUM_PROJECTS | Yes | Limit number of projects on home page. |
---
## Customize metadata for individual pages
```ts
// src/consts.ts
export const ABOUT: Metadata = {
TITLE: "About",
DESCRIPTION: "Astro Micro is a fork of Astro Nano.",
};
```
| Field | Req | Description |
| :---------- | :-- | :--------------------------------------------- |
| TITLE | Yes | Displayed in browser tab. Used in SEO and RSS. |
| DESCRIPTION | Yes | Used in SEO and RSS. |
---
## Add your social media links
```ts
// src/consts.ts
export const SOCIALS: Socials = [
{
NAME: "twitter-x",
HREF: "https://twitter.com/boogerbuttcheeks",
},
{
NAME: "github",
HREF: "https://github.com/trevortylerlee",
},
{
NAME: "linkedin",
HREF: "https://www.linkedin.com/in/trevortylerlee",
},
];
```
| Field | Req | Description |
| :---- | :-- | :-------------------------------------- |
| NAME | Yes | Displayed in contact section as a link. |
| HREF | Yes | External url to social media profile. |
## Deploy the site
To set up RSS and Giscus, it's easier if the site is deployed and has a URL for you to use. Instantly deploy to Vercel or Netlify by clicking the buttons below.
<div class="flex gap-2">
<a target="_blank" aria-label="Deploy with Vercel" href="https://vercel.com/new/clone?repository-url=https://github.com/trevortylerlee/astro-micro">
<img src="/deploy_vercel.svg" />
</a>
<a target="_blank" aria-label="Deploy with Netlify" href="https://app.netlify.com/start/deploy?repository=https://github.com/trevortylerlee/astro-micro">
<img src="/deploy_netlify.svg" />
</a>
</div>
To deploy manually see [Astro's docs](https://docs.astro.build/en/guides/deploy/).
## Set up RSS
Change the `site` option to the deployed site's URL.
```js
// astro.config.mjs
export default defineConfig({
site: "https://astro-micro.vercel.app",
integrations: [tailwind(), sitemap(), mdx(), pagefind()],
markdown: {
shikiConfig: {
theme: "css-variables",
},
},
});
```
## Set up Giscus
Follow the steps at [giscus.app](https://giscus.app). Once you get your custom Giscus script from that site, go to `Giscus.astro` and replace that script with your own.
```js
// src/components/Giscus.astro
<script
is:inline
src="https://giscus.app/client.js"
data-repo="trevortylerlee/astro-micro"
data-repo-id="R_kgDOL_6l9Q"
data-category="Announcements"
data-category-id="DIC_kwDOL_6l9c4Cfk55"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme="preferred_color_scheme"
data-lang="en"
data-loading="lazy"
crossorigin="anonymous"
async
></script>
```
To change the Giscus themes used, edit the `setGiscusTheme` function in `Head.astro`.
```js
// src/components/Head.astro
const setGiscusTheme = () => {
const giscus = document.querySelector(".giscus-frame");
const isDark = document.documentElement.classList.contains("dark");
if (giscus) {
const url = new URL(giscus.src);
// Change "dark" and "light" to other Giscus themes
url.searchParams.set("theme", isDark ? "dark" : "light");
giscus.src = url.toString();
}
};
```

View file

@ -0,0 +1,69 @@
---
title: "Blog Collection"
description: "How to add posts to the blog."
date: "2024-03-21"
tags:
- tutorial
---
---
## Working with the `blog` collection:
The `blog` collection is found in `src/content/blog`.
```
📁 /src/content/blog
└── 📁 post-1
└── 📄 index.md
└── 📁 post-2
└── 📄 index.mdx
```
In the above example, two blog posts will be generated with the folder name representing the id.
- `https://example.com/blog/post-1`
- `https://example.com/blog/post-2`
---
## Provide metadata
Metadata is required for each post.
```astro
---
title: "Blog Collection";
description: "How to add posts to the blog.";
date: "2024-03-21";
draft: false;
tags:
- tutorial
---
```
| Field | Req | Type | Remarks |
| :---------- | :-- | :------ | :----------------------------------------------- |
| title | Yes | string | Title of the content. Used in SEO and RSS. |
| description | Yes | string | Description of the content. Used in SEO and RSS. |
| date | Yes | string | Must be a valid date string (able to be parsed). |
| draft | No | boolean | If draft: true, content will not be published. |
| tags | No | string array | Tags to organize content |
---
All that's left to do is write the content under the metadata.
```astro
---
title: "Blog Collection";
description: "How to add posts to the blog.";
date: "2024-03-21";
draft: false;
tags:
- tutorial
---
## Working with the blog collection
<!-- content -->
```

View file

@ -0,0 +1,64 @@
---
title: "Projects Collection"
description: "How to add projects to your portfolio."
date: "2024-03-20"
tags:
- tutorial
---
---
## Working with the `projects` collection
The `projects` collections is found in `src/content/projects`.
```
📁 /src/content/projects
└── 📁 project-1
└── 📄 index.md
└── 📁 project-2
└── 📄 index.mdx
```
In the above example, two project posts will be generated with the folder name representing the id.
- `https://example.com/projects/project-1`
- `https://example.com/projects/project-2`
---
## Provide metadata
```astro
---
title: "Astro Micro";
description: "Astro Micro is an accessible theme for Astro.";
date: "2024-03-20";
draft: false;
---
```
| Field | Req | Type | Remarks |
| :---------- | :-- | :------ | :----------------------------------------------- |
| title | Yes | string | Title of the content. Used in SEO and RSS. |
| description | Yes | string | Description of the content. Used in SEO and RSS. |
| date | Yes | string | Must be a valid date string (able to be parsed). |
| draft | No | boolean | If draft: true, content will not be published. |
| demoURL | No | string | Link to live project demo, if available. |
| repoURL | No | string | Link to project repo, if available. |
---
All that's left to do is write the content under the metadata.
```astro
---
title: "Astro Micro";
description: "Astro Micro is an accessible theme for Astro";
date: "2024-03-20";
draft: false;
---
## Astro Micro 🔬
<!-- content -->
```

View file

@ -0,0 +1,400 @@
---
title: "Markdown syntax guide"
description: "Get started writing content in Markdown."
date: "2024-03-17"
tag:
- reference
---
import Callout from "@/components/Callout.astro";
---
## Headings
To create headings, use hash symbols (#) followed by a space. The number of hash symbols indicates the heading level.
<Callout>
Use `h2` tags instead of `h1` tags in the post. Too many `h1` tags on a single
page can impact SEO. The post title serves as the `h1`.
</Callout>
```md
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
```
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
---
## Paragraphs
To create paragraphs, use a blank line to separate one or more lines of text.
{/* prettier-ignore */}
```md
<!-- empty line -->
I love Star Wars.
<!-- empty line -->
My favourite is Episode III Revenge of the Sith.
<!-- empty line -->
```
I love Star Wars.
My favourite is Episode III Revenge of the Sith.
---
## Italic
Use one asterisk \(\*\) or underscore \(\_\) to italicize text.
{/* prettier-ignore */}
```md
I *love* Star Wars.
My _favourite_ is Episode III Revenge of the Sith.
```
I _love_ Star Wars.
My _favourite_ is Episode III Revenge of the Sith.
---
## Bold
Use two asterisks \(\*\) or underscores \(\_\) to bold text.
{/* prettier-ignore */}
```md
I **love** Star Wars.
My __favourite__ is Episode III Revenge of the Sith.
```
I **love** Star Wars.
My **favourite** is Episode III Revenge of the Sith.
---
## Italic and Bold
Use three asterisks \(\*\) or underscores \(\_\) to both bold and italicize text.
{/* prettier-ignore */}
```md
I ***love*** Star Wars.
My ___favourite___ is Episode III Revenge of the Sith.
```
I **_love_** Star Wars.
My **_favourite_** is Episode III Revenge of the Sith.
---
## Horizontal Rule
Use three hyphens \(\-\), asterisks \(\*\), or underscores \(\_\) to create a horizontal rule.
{/* prettier-ignore */}
```md
<!-- empty line -->
---
<!-- empty line -->
***
<!-- empty line -->
___
<!-- empty line -->
```
---
---
---
## Links
To create a link, the link text in brackets \(\[\]\) and then follow it immediately with the URL in parentheses \(\(\)\).
```md
Micro is a fork of [astro-nano](https://github.com/markhorn-dev/astro-nano).
```
Micro is a fork of [astro-nano](https://github.com/markhorn-dev/astro-nano).
---
## Ordered Lists
To create an ordered list, add line items with numbers followed by periods. Use an indent to create a nested list.
```md
1. Item 1
2. Item 2
1. Sub item 1
2. Sub item 2
3. Item 3
```
1. Item 1
2. Item 2
1. Sub item 1
2. Sub item 2
3. Item 3
---
## Unordered List
To create an unordered list, add a hyphen \(\-\), an asterisk \(\*\), or a plus sign \(\+\) in front of line items. Don't mix. Use an indent to create a nested list.
```md
- Item 1
- Item 2
- Sub item 1
- Sub item 2
- Item 3
```
- Item 1
- Item 2
- Sub item 1
- Sub item 2
- Item 3
---
## Images
To add an image, add an exclamation mark (!), followed by alt text in brackets ([]), and the path or URL to the image asset in parentheses.
```md
![altText](pathToImage)
```
### Relative
Use the `![altText](./image.*)` pattern relative to the same folder as the markdown file. Notice the period.
```md
![X-Wing Starfighter](./x-wing.jpeg)
```
![X-Wing Starfighter](./x-wing.jpeg)
### Public Image
Use the `![altText](/image.*)` pattern relative to the public folder. No period.
```md
![Y-Wing Starfighter](/y-wing.jpeg)
```
![Y-Wing Starfighter](/y-wing.jpeg)
### External Image
Use the `![altText](url)` pattern.
```md
![Naboo N-1 Starfighter](https://raw.githubusercontent.com/trevortylerlee/n1/main/n1.jpeg)
```
![Naboo N-1 Starfighter](https://raw.githubusercontent.com/trevortylerlee/n1/main/n1.jpeg)
---
## Blockquotes
To add a blockquote add the greater-than character \(\>\) before a paragraph. For multi-line blockquotes, add additional greater-than character for each line and include an empty spacer line.
```md
> So this is how liberty dies...
>
> with thunderous applause.
```
> So this is how liberty dies...
>
> with thunderous applause.
---
## Strikethrough
Use a tilde \(\~\) symbol to strikethrough text.
```md
~I don't like sand.~ It's coarse and rough and irritating.
```
~I don't like sand.~ It's coarse and rough and irritating.
---
## Subscript
Use the `<sub>` tag to denote subscript.
```md
H<sub>2</sub>O
```
H<sub>2</sub>O
---
## Superscript
Use the `<sup>` tag to denote superscript.
```md
E=mc<sup>2</sup>
```
E=mc<sup>2</sup>
---
## Keyboard
Use the `<kbd>` tag to denote keys on the keyboard.
```md
<kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd>
```
<kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd>
---
## Abbreviate
Use the `<abbr>` tag to denote abbreviation.
```md
<abbr title="Graphics Interchange Format">GIF</abbr>
```
<abbr title="Graphics Interchange Format">GIF</abbr>
---
### Highlight
Use the `<mark>` tag to denote highlighted text.
```md
Do or do not. <mark>There is no try.</mark>
```
Do or do not. <mark>There is no try.</mark>
---
## Task Lists
Combine a list with square brackets ([]) representing a checkbox. Typing `x` inside the brackets marks the task as complete.
```md
- [x] Build a lightsaber
- [ ] Pass the Jedi Trials
- [ ] Train a padawan
```
- [x] Build a lightsaber
- [ ] Pass the Jedi Trials
- [ ] Train a padawan
---
## Tables
Use three or more hyphens (-) for the column headers and use pipes (|) to separate each column. You can align text in the columns to the left, right, or center by adding a colon (:) to the left, right, or on both side of the hyphens.
```md
| Item | Count |
| :----- | ----: |
| X-Wing | 1 |
| Y-Wing | 2 |
| A-Wing | 3 |
```
| Item | Count |
| :----- | ----: |
| X-Wing | 1 |
| Y-Wing | 2 |
| A-Wing | 3 |
---
## Footnotes
Add a caret (^) and an identifier inside brackets \(\[\^1\]\). Identifiers can be numbers or words but can't contain spaces or tabs.
```md
Here's a footnote, [^1] and here's another one. [^2]
[^1]: This is the first footnote.
[^2]: This is the second footnote.
```
Here's a footnote, [^1] and here's another one. [^2]
[^1]: This is the first footnote.
[^2]: This is the second footnote.
See the bottom of this page to view the footnotes.
---
## Code
To denote a word or phrase as code, enclose it in backticks (`).
```md
`package.json`
```
`package.json`
---
## Code Blocks
Denote a code block by wrapping a section of valid code in triple backticks (`). To enable syntax highlighting, type the shorthand symbol for the language after the first three backticks. [Reference for shorthand symbols](https://shiki.style/languages).
````
```js
function hello() {
console.log("hello world");
}
```
````
```js
function hello() {
console.log("hello world");
}
```
---
## Conclusion
Refer to [markdownguide.org](https://www.markdownguide.org/) for best practices as well as extended syntax.
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View file

@ -0,0 +1,10 @@
---
---
<button
onclick="alert(`You clicked the button!`)"
class="border border-black/10 p-2 transition-colors duration-300 ease-in-out hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/10"
>
Relative Button
</button>

View file

@ -0,0 +1,73 @@
---
title: "MDX syntax guide"
description: "Use interactive components in Markdown."
date: "2024-03-16"
tags:
- reference
---
import Callout from "@/components/Callout.astro";
---
MDX is an extension of Markdown with the ability to import `.astro`,
`.jsx`, `.tsx` and other framework components you have integrated.
This guide covers the basics of MDX syntax and how to use it, as well as a few examples.
## Example 1
Importing a component from the `/components` directory.
This component accepts a Javascript date object and format it as a string.
```astro
import DateComp from "../../../components/FormattedDate.astro";
<DateComp date={new Date()} />
```
import FormattedDate from "../../../components/FormattedDate.astro";
<FormattedDate date={new Date()} />
---
## Example 2
Importing a component from a relative path to your content.
This component displays an alert when the button is clicked.
```astro
import RelativeComponent from "./component.astro";
<RelativeComponent />
```
import RelativeComponent from "./component.astro";
<RelativeComponent />
---
By default Micro has zero frameworks installed. If you install a framework, components of that framework can be used in MDX files.
<Callout>
Don't forget to use [client
directives](https://docs.astro.build/en/reference/directives-reference/#client-directives)
to make framework components interactive.
</Callout>
```astro
<ReactComponent client:load />
```
---
## More Links
- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
- [Astro Framework Integrations](https://docs.astro.build/en/guides/integrations-guide)
- [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
- [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives)

View file

@ -0,0 +1,9 @@
---
title: "Year sorting example"
description: "Nano groups posts by year."
date: "2023-12-31"
tags:
- example
---
This post is to demonstrate the year sorting capabilities.

View file

@ -0,0 +1,12 @@
---
title: "Draft example"
description: "Setting draft flag to true to hide this post."
date: "2022-12-31"
draft: false
tags:
- example
---
This post also demonstrates the year sorting capabilities.
Try setting this file's metadata to `draft: true`.

View file

@ -0,0 +1,11 @@
---
title: "Chronological pagination example"
description: "Pagination works regardless of folder name."
date: "2024-03-21"
tags:
- example
---
This post should show up in proper chronological order even though its folder comes last in the `content/blog` directory.
The `Previous Post` and `Next Post` buttons under each blog post should also keep the proper chronological order, based on the frontmatter `date` field.

View file

@ -0,0 +1,79 @@
---
title: "Astro Sphere"
description: "Portfolio and blog build with astro."
date: "03/18/2024"
demoURL: "https://astro-sphere-demo.vercel.app"
repoURL: "https://github.com/markhorn-dev/astro-sphere"
---
![Astro Sphere](/astro-sphere.jpg)
Astro Sphere is a static, minimalist, lightweight, lightning fast portfolio and blog theme based on Mark Horn's personal website.
It is primarily Astro, Tailwind and Typescript, with a very small amount of SolidJS for stateful components.
## 🚀 Deploy your own
<div class="flex gap-2">
<a target="_blank" aria-label="Deploy with Vercel" href="https://vercel.com/new/clone?repository-url=https://github.com/markhorn-dev/astro-sphere">
<img src="/deploy_vercel.svg" />
</a>
<a target="_blank" aria-label="Deploy with Netlify" href="https://app.netlify.com/start/deploy?repository=https://github.com/markhorn-dev/astro-sphere">
<img src="/deploy_netlify.svg" />
</a>
</div>
## 📋 Features
- ✅ 100/100 Lighthouse performance
- ✅ Responsive
- ✅ Accessible
- ✅ SEO-friendly
- ✅ Typesafe
- ✅ Minimal style
- ✅ Light/Dark Theme
- ✅ Animated UI
- ✅ Tailwind styling
- ✅ Auto generated sitemap
- ✅ Auto generated RSS Feed
- ✅ Markdown support
- ✅ MDX Support (components in your markdown)
- ✅ Searchable content (posts and projects)
## 💯 Lighthouse score
![Astro Sphere Lighthouse Score](/lighthouse-nano.jpg)
## 🕊️ Lightweight
All pages under 100kb (including fonts)
## ⚡︎ Fast
Rendered in ~40ms on localhost
## 📄 Configuration
The blog posts on the demo serve as the documentation and configuration.
## 💻 Commands
All commands are run from the root of the project, from a terminal:
Replace npm with your package manager of choice. `npm`, `pnpm`, `yarn`, `bun`, etc
| Command | Action |
| :------------------------ | :------------------------------------------------ |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run sync` | Generates TypeScript types for all Astro modules. |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
| `npm run lint` | Run ESLint |
| `npm run lint:fix` | Auto-fix ESLint issues |
## 🏛️ License
MIT

View file

@ -0,0 +1,82 @@
---
title: "Astro Nano"
description: "Minimal portfolio and blog build with astro and no frameworks."
date: "2024-03-26"
demoURL: "https://astro-nano-demo.vercel.app"
repoURL: "https://github.com/markhorn-dev/astro-nano"
---
![Astro Nano](/astro-nano.png)
Astro Nano is a static, minimalist, lightweight, lightning fast portfolio and blog theme.
Built with Astro, Tailwind and Typescript, and no frameworks.
It was designed as an even more minimal theme than Mark Horn's popular theme [Astro Sphere](https://github.com/markhorn-dev/astro-sphere)
## 🚀 Deploy your own
<div class="flex gap-2">
<a target="_blank" aria-label="Deploy with Vercel" href="https://vercel.com/new/clone?repository-url=https://github.com/markhorn-dev/astro-nano">
<img src="/deploy_vercel.svg" />
</a>
<a target="_blank" aria-label="Deploy with Netlify" href="https://app.netlify.com/start/deploy?repository=https://github.com/markhorn-dev/astro-nano">
<img src="/deploy_netlify.svg" />
</a>
</div>
## 📋 Features
- ✅ 100/100 Lighthouse performance
- ✅ Responsive
- ✅ Accessible
- ✅ SEO-friendly
- ✅ Typesafe
- ✅ Minimal style
- ✅ Light/Dark Theme
- ✅ Animated UI
- ✅ Tailwind styling
- ✅ Auto generated sitemap
- ✅ Auto generated RSS Feed
- ✅ Markdown support
- ✅ MDX Support (components in your markdown)
## 💯 Lighthouse score
![Astro Nano Lighthouse Score](/astro-nano-lighthouse.jpg)
## 🕊️ Lightweight
No frameworks or added bulk
## ⚡︎ Fast
Rendered in ~40ms on localhost
## 📄 Configuration
The blog posts on the demo serve as the documentation and configuration.
## 💻 Commands
All commands are run from the root of the project, from a terminal:
Replace npm with your package manager of choice. `npm`, `pnpm`, `yarn`, `bun`, etc
| Command | Action |
| :------------------------ | :------------------------------------------------ |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run dev:network` | Starts local dev server on local network |
| `npm run sync` | Generates TypeScript types for all Astro modules. |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run preview:network` | Preview build on local network |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
| `npm run lint` | Run ESLint |
| `npm run lint:fix` | Auto-fix ESLint issues |
## 🏛️ License
MIT

View file

@ -0,0 +1,45 @@
---
title: "Astro Micro"
description: "Astro Micro is an accessible and lightweight blog."
date: "2024-05-26"
demoURL: "https://astro-micro.vercel.app"
repoURL: "https://github.com/trevortylerlee/astro-micro"
---
![Astro Micro](/astro-micro.jpg)
Astro Micro is an accessible theme for Astro. It's a fork of Mark Horn's popular theme Astro Nano. Like Nano, Micro comes with zero frameworks installed.
Micro adds features like [Pagefind](https://pagefind.app) for search, [Giscus](https://giscus.app) for comments, and more. For a full list of changes, see this [blog post](/blog/00-micro-changelog).
Micro still comes with everything great about Nano — full type safety, a sitemap, an RSS feed, and Markdown + MDX support. Styled with TailwindCSS and preconfigured with system, light, and dark themes.
Visit [Astro Micro on Github](https://github.com/trevortylerlee/astro-micro) to get started.
## 🚀 Deploy your own
<div class="flex gap-2">
<a target="_blank" aria-label="Deploy with Vercel" href="https://vercel.com/new/clone?repository-url=https://github.com/trevortylerlee/astro-micro">
<img src="/deploy_vercel.svg" />
</a>
<a target="_blank" aria-label="Deploy with Netlify" href="https://app.netlify.com/start/deploy?repository=https://github.com/trevortylerlee/astro-micro">
<img src="/deploy_netlify.svg" />
</a>
</div>
## 📋 Features
Everything in [Astro Nano](https://github.com/markhorn-dev/astro-nano) plus:
- Pagefind search
- Giscus comments
- Callout component
- Table of contents component
- Pagination component
- 404 page
and [more](/blog/00-micro-changelog).
## 💯 Lighthouse score
![Astro Nano Lighthouse Score](/astro-nano-lighthouse.jpg)

37
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,37 @@
---
import Head from "@components/Head.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Pagefind from "@components/PageFind.astro";
import { SITE } from "@consts";
type Props = {
title: string;
description: string;
};
const { title, description } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<Head title={`${title} | ${SITE.TITLE}`} description={description} />
</head>
<body>
<noscript>
<style>
.animate{
opacity:1;
translate: var(--tw-translate-x) calc(var(--spacing) * 0);
}
</style>
</noscript>
<Header />
<main>
<slot />
</main>
<Footer />
<Pagefind />
</body>
</html>

21
src/lib/utils.ts Normal file
View file

@ -0,0 +1,21 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(date: Date) {
return Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(date);
}
export function readingTime(html: string) {
const textOnly = html.replace(/<[^>]+>/g, "");
const wordCount = textOnly.split(/\s+/).length;
const readingTimeMinutes = (wordCount / 200 + 1).toFixed();
return `${readingTimeMinutes} min read`;
}

19
src/pages/404.astro Normal file
View file

@ -0,0 +1,19 @@
---
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
import BackToPrevious from "@components/BackToPrevious.astro";
import { SITE } from "@consts";
---
<Layout title="404" description={SITE.DESCRIPTION}>
<Container>
<div class="mt-16 grid place-items-center gap-3">
<h4 class="animate text-2xl font-semibold text-black dark:text-white">
404: Page not found
</h4>
<span class="animate">
<BackToPrevious href="/">Go to home page</BackToPrevious>
</span>
</div>
</Container>
</Layout>

View file

@ -0,0 +1,100 @@
---
import { type CollectionEntry, getCollection, render } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
import FormattedDate from "@components/FormattedDate.astro";
import { readingTime } from "@lib/utils";
import BackToPrevious from "@components/BackToPrevious.astro";
import PostNavigation from "@components/PostNavigation.astro";
import TableOfContents from "@components/TableOfContents.astro";
import Giscus from "@components/Giscus.astro";
export async function getStaticPaths() {
const posts = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return posts.map((post) => ({
params: { id: post.id },
props: post,
}));
}
type Props = CollectionEntry<"blog">;
const posts = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
function getNextPost() {
let postIndex;
for (const post of posts) {
if (post.id === Astro.params.id) {
postIndex = posts.indexOf(post);
return posts[postIndex + 1];
}
}
}
function getPrevPost() {
let postIndex;
for (const post of posts) {
if (post.id === Astro.params.id) {
postIndex = posts.indexOf(post);
return posts[postIndex - 1];
}
}
}
const nextPost = getNextPost();
const prevPost = getPrevPost();
const post = Astro.props;
const { Content, headings } = await render(post);
---
<Layout title={post.data.title} description={post.data.description}>
<Container>
<div class="animate">
<BackToPrevious href="/blog">Back to blog</BackToPrevious>
</div>
<div class="my-10 space-y-1">
<div class="animate flex items-center gap-1.5">
<div class="font-base text-sm">
<FormattedDate date={post.data.date} />
</div>
&bull;
{post.body && (
<div class="font-base text-sm">
{readingTime(post.body)}
</div>
)}
</div>
<h1 class="animate text-3xl font-semibold text-black dark:text-white">
{post.data.title}
</h1>
{
post.data.tags && post.data.tags?.length > 0 ? (
<div class="animate flex gap-2 pt-1">
{post.data.tags.map((tag) => (
<a
href={`/tags/${tag}`}
class="rounded-sm border border-black/15 px-2 py-1 text-sm transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
{tag}
</a>
))}
</div>
) : null
}
</div>
{headings.length > 0 && <TableOfContents headings={headings} />}
<article class="animate">
<Content />
<div class="mt-24">
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
</div>
<div class="mt-24">
<Giscus />
</div>
</article>
</Container>
</Layout>

View file

@ -0,0 +1,55 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
import ArrowCard from "@components/ArrowCard.astro";
import { BLOG } from "@consts";
const data = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
type Acc = {
[year: string]: CollectionEntry<"blog">[];
};
const posts = data.reduce((acc: Acc, post) => {
const year = post.data.date.getFullYear().toString();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(post);
return acc;
}, {});
const years = Object.keys(posts).sort((a, b) => parseInt(b) - parseInt(a));
---
<Layout title={BLOG.TITLE} description={BLOG.DESCRIPTION}>
<Container>
<aside data-pagefind-ignore>
<div class="space-y-10">
<div class="space-y-4">
{
years.map((year) => (
<section class="animate space-y-4">
<div class="font-semibold text-black dark:text-white">
{year}
</div>
<div>
<ul class="not-prose flex flex-col gap-4">
{posts[year].map((post) => (
<li>
<ArrowCard entry={post} />
</li>
))}
</ul>
</div>
</section>
))
}
</div>
</div>
</aside>
</Container>
</Layout>

145
src/pages/index.astro Normal file
View file

@ -0,0 +1,145 @@
---
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
import { SITE, HOME, SOCIALS } from "@consts";
import ArrowCard from "@components/ArrowCard.astro";
import Link from "@components/Link.astro";
import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
const blog = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
.slice(0, SITE.NUM_POSTS_ON_HOMEPAGE);
const projects: CollectionEntry<"projects">[] = (
await getCollection("projects")
)
.filter((project) => !project.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
.slice(0, SITE.NUM_PROJECTS_ON_HOMEPAGE);
---
<Layout title={HOME.TITLE} description={HOME.DESCRIPTION}>
<Container>
<aside data-pagefind-ignore>
<h1 class="font-semibold text-black dark:text-white">
Introducing Astro Micro 🔬
</h1>
<div class="space-y-16">
<section>
<article class="space-y-4">
<span>
<p>
Astro Micro is an accessible theme for <Link
href="https://astro.build/">Astro</Link
>. It's a fork of
<Link href="https://github.com/markhorn-dev">
Mark Horn's
</Link> popular theme <Link
href="https://astro.build/themes/details/astronano/"
>Astro Nano</Link
>. Like Nano, Micro comes with zero frameworks installed.
</p>
<p>
Micro adds features like <Link href="https://pagefind.app/"
>Pagefind</Link
> for search, <Link href="https://giscus.app">Giscus</Link> for comments,
and more. For a full list of changes, see this <Link
href="/blog/00-micro-changelog">blog post</Link
>.
</p>
</span>
<span class="animate">
<p>
Micro still comes with everything great about Nano — full type
safety, a sitemap, an RSS feed, and Markdown + MDX support.
Styled with TailwindCSS and preconfigured with system, light,
and dark themes.
</p>
<p>
Visit
<Link href="https://github.com/trevortylerlee/astro-micro">
Astro Micro on GitHub
</Link>
to fork the repository to get started.
</p>
</span>
</article>
</section>
<section class="animate space-y-6">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<h2 class="font-semibold text-black dark:text-white">
Latest posts
</h2>
<Link href="/blog"> See all posts </Link>
</div>
<ul class="not-prose flex flex-col gap-4">
{
blog.map((post) => (
<li>
<ArrowCard entry={post} />
</li>
))
}
</ul>
</section>
<section class="animate space-y-6">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<h2 class="font-semibold text-black dark:text-white">
Recent projects
</h2>
<Link href="/projects"> See all projects </Link>
</div>
<ul class="not-prose flex flex-col gap-4">
{
projects.map((project) => (
<li>
<ArrowCard entry={project} />
</li>
))
}
</ul>
</section>
<section class="animate space-y-4">
<h2 class="font-semibold text-black dark:text-white">
Let's Connect
</h2>
<article>
<p>
If you want to get in touch with me about something or just to say
hi, reach out on social media or send me an email.
</p>
</article>
<ul class="not-prose flex flex-wrap gap-2">
{
SOCIALS.map((SOCIAL) => (
<li class="flex gap-x-2 text-nowrap">
<Link
href={SOCIAL.HREF}
external
aria-label={`${SITE.TITLE} on ${SOCIAL.NAME}`}
>
{SOCIAL.NAME}
</Link>
{"/"}
</li>
))
}
<li class="line-clamp-1">
<Link
href={`mailto:${SITE.EMAIL}`}
aria-label={`Email ${SITE.TITLE}`}
>
{SITE.EMAIL}
</Link>
</li>
</ul>
</section>
</div>
</aside>
</Container>
</Layout>

View file

@ -0,0 +1,69 @@
---
import { type CollectionEntry, getCollection, render } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
import FormattedDate from "@components/FormattedDate.astro";
import { readingTime } from "@lib/utils";
import BackToPrevious from "@components/BackToPrevious.astro";
import Link from "@components/Link.astro";
import TableOfContents from "@components/TableOfContents.astro";
export async function getStaticPaths() {
const projects = (await getCollection("projects"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return projects.map((project) => ({
params: { id: project.id },
props: project,
}));
}
type Props = CollectionEntry<"projects">;
const project = Astro.props;
const { Content, headings } = await render(project);
---
<Layout title={project.data.title} description={project.data.description}>
<Container>
<div class="animate">
<BackToPrevious href="/projects">Back to projects</BackToPrevious>
</div>
<div class="animate my-10 space-y-1">
<div class="flex items-center gap-1.5">
<div class="font-base text-sm">
<FormattedDate date={project.data.date} />
</div>
&bull;
{project.body && (
<div class="font-base text-sm">
{readingTime(project.body)}
</div>
)}
</div>
<h1 class="text-3xl font-semibold text-black dark:text-white">
{project.data.title}
</h1>
{
(project.data.demoURL || project.data.repoURL) && (
<nav class="flex gap-1">
{project.data.demoURL && (
<Link href={project.data.demoURL} external>
demo
</Link>
)}
{project.data.demoURL && project.data.repoURL && <span>/</span>}
{project.data.repoURL && (
<Link href={project.data.repoURL} external>
repo
</Link>
)}
</nav>
)
}
</div>
<TableOfContents headings={headings} />
<article class="animate">
<Content />
</article>
</Container>
</Layout>

View file

@ -0,0 +1,32 @@
---
import { getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
import ArrowCard from "@components/ArrowCard.astro";
import { PROJECTS } from "@consts";
const projects = (await getCollection("projects"))
.filter((project) => !project.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<Layout title={PROJECTS.TITLE} description={PROJECTS.DESCRIPTION}>
<Container>
<aside data-pagefind-ignore>
<div class="space-y-10">
<div class="animate font-semibold text-black dark:text-white">
Projects
</div>
<ul class="animate not-prose flex flex-col gap-4">
{
projects.map((project) => (
<li>
<ArrowCard entry={project} />
</li>
))
}
</ul>
</div>
</aside>
</Container>
</Layout>

27
src/pages/rss.xml.js Normal file
View file

@ -0,0 +1,27 @@
import rss from "@astrojs/rss";
import { SITE } from "@consts";
import { getCollection } from "astro:content";
export async function GET(context) {
const blog = (await getCollection("blog")).filter((post) => !post.data.draft);
const projects = (await getCollection("projects")).filter(
(project) => !project.data.draft,
);
const items = [...blog, ...projects].sort(
(a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(),
);
return rss({
title: SITE.TITLE,
description: SITE.DESCRIPTION,
site: context.site,
items: items.map((item) => ({
title: item.data.title,
description: item.data.description,
pubDate: item.data.date,
link: `/${item.collection}/${item.id}/`,
})),
});
}

View file

@ -0,0 +1,44 @@
---
import { getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
import ArrowCard from "@components/ArrowCard.astro";
import BackToPrevious from "@components/BackToPrevious.astro";
export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => !data.draft);
// Get unique tags
const tags = [...new Set(posts.flatMap((post) => post.data.tags || []))];
// Create paths for each tag
return tags.map((tag) => ({
params: { id: tag },
props: {
posts: posts.filter((post) => post.data.tags?.includes(tag)),
},
}));
}
const { id } = Astro.params;
const { posts } = Astro.props;
// Sort posts by date, most recent first
const sortedPosts = posts.sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
);
---
<Layout title={`Tag: ${id}`} description={`Posts with the tag: ${id}`}>
<Container>
<div class="space-y-10" data-pagefind-ignore>
<BackToPrevious href="/tags"> All tags </BackToPrevious>
<h1 class="animate font-semibold text-black dark:text-white">
Posts tagged with "{id}"
</h1>
<ul class="animate flex flex-col gap-4">
{sortedPosts.map((post) => <ArrowCard entry={post} />)}
</ul>
</div>
</Container>
</Layout>

View file

@ -0,0 +1,31 @@
---
import { getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import Container from "@components/Container.astro";
const posts = await getCollection("blog", ({ data }) => !data.draft);
const tags = [...new Set(posts.flatMap((post) => post.data.tags || []))].sort();
---
<Layout title="Tags" description="List of tags used.">
<Container>
<div class="space-y-10">
<h1 class="animate font-semibold">All Tags</h1>
<div class="animate flex flex-wrap gap-2">
{
tags.map((tag) => (
<a
href={`/tags/${tag}`}
class="rounded-sm border border-black/15 px-2 py-1 text-sm transition-colors duration-300 ease-in-out hover:bg-black/5 hover:text-black focus-visible:bg-black/5 focus-visible:text-black dark:border-white/20 dark:hover:bg-white/5 dark:hover:text-white dark:focus-visible:bg-white/5 dark:focus-visible:text-white"
>
{tag}{" "}
<span class="text-sm text-gray-600">
({posts.filter((post) => post.data.tags?.includes(tag)).length})
</span>
</a>
))
}
</div>
</div>
</Container>
</Layout>

149
src/styles/global.css Normal file
View file

@ -0,0 +1,149 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: Geist Sans, ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, 'Liberation Mono', 'Courier New', monospace;
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer utilities {
html {
overflow-y: auto;
color-scheme: light;
scroll-padding-top: 100px;
}
html.dark {
color-scheme: dark;
}
html,
body {
@apply size-full;
}
body {
@apply font-sans antialiased;
@apply flex flex-col;
@apply bg-neutral-100 dark:bg-neutral-900;
@apply text-black/75 dark:text-white/75;
}
header {
@apply fixed left-0 right-0 top-0 z-50 py-6;
@apply bg-neutral-100/75 dark:bg-neutral-900/75;
@apply saturate-200 backdrop-blur-xs;
}
main {
@apply flex-1 py-32;
}
footer {
@apply py-6 text-sm;
}
article {
@apply prose prose-neutral max-w-full dark:prose-invert prose-img:mx-auto prose-img:my-auto;
@apply prose-headings:font-semibold;
@apply prose-headings:text-black dark:prose-headings:text-white;
}
}
@layer utilities {
article a {
@apply font-sans text-current underline underline-offset-[3px];
@apply decoration-black/30 dark:decoration-white/30;
@apply transition-colors duration-300 ease-in-out;
}
article a:hover {
@apply text-black dark:text-white;
@apply decoration-black/50 dark:decoration-white/50;
}
}
.animate {
@apply -translate-y-3 opacity-0;
@apply transition-all duration-300 ease-out;
}
.animate.show {
@apply translate-y-0 opacity-100;
}
html #back-to-top {
@apply pointer-events-none opacity-0;
}
html.scrolled #back-to-top {
@apply pointer-events-auto opacity-100;
}
/* shiki config */
pre {
@apply border border-black/15 py-5 dark:border-white/20;
}
:root {
--astro-code-foreground: #09090b;
--astro-code-background: #fafafa;
--astro-code-token-comment: #a19595;
--astro-code-token-keyword: #f47067;
--astro-code-token-string: #00a99a;
--astro-code-token-function: #429996;
--astro-code-token-constant: #2b70c5;
--astro-code-token-parameter: #4e8fdf;
--astro-code-token-string-expression: #ae42a0;
--astro-code-token-punctuation: #8996a3;
--astro-code-token-link: #8d85ff;
}
.dark {
--astro-code-foreground: #fafafa;
--astro-code-background: #09090b;
--astro-code-token-comment: #a19595;
--astro-code-token-keyword: #f47067;
--astro-code-token-string: #00a99a;
--astro-code-token-function: #6eafad;
--astro-code-token-constant: #b3cceb;
--astro-code-token-parameter: #4e8fdf;
--astro-code-token-string-expression: #bf7db6;
--astro-code-token-punctuation: #8996a3;
--astro-code-token-link: #8d85ff;
}
/* copy code button on codeblocks */
.copy-code {
@apply absolute right-3 top-3 grid size-9 place-content-center rounded-sm border border-black/15 bg-neutral-100 text-center duration-300 ease-in-out dark:border-white/20 dark:bg-neutral-900;
}
.copy-code:hover {
@apply bg-[#E9E9E9] transition-colors dark:bg-[#232323];
}
.copy-code:active {
@apply scale-90 transition-transform;
}

17
src/types.ts Normal file
View file

@ -0,0 +1,17 @@
export type Site = {
TITLE: string;
DESCRIPTION: string;
EMAIL: string;
NUM_POSTS_ON_HOMEPAGE: number;
NUM_PROJECTS_ON_HOMEPAGE: number;
};
export type Metadata = {
TITLE: string;
DESCRIPTION: string;
};
export type Socials = {
NAME: string;
HREF: string;
}[];