[READ-ONLY] Mirror of https://github.com/flo-bit/blog-template. minimalistic astro blog template flo-bit.dev/blog-template/
astro blog template
0

Configure Feed

Select the types of activity you want to include in your feed.

some refactor, change most components from svelte -> astro

Florian (Oct 30, 2024, 12:39 AM +0100) 5b980d0c 9dc4fc74

+442 -415
+10
package-lock.json
··· 19 19 "astro": "^4.16.7", 20 20 "astro-og-canvas": "^0.5.4", 21 21 "svelte": "^5.1.1", 22 + "tailwind-merge": "^2.5.4", 22 23 "tailwindcss": "^3.4.14", 23 24 "typescript": "^5.6.3" 24 25 }, ··· 6837 6838 "peerDependencies": { 6838 6839 "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", 6839 6840 "typescript": "^4.9.4 || ^5.0.0" 6841 + } 6842 + }, 6843 + "node_modules/tailwind-merge": { 6844 + "version": "2.5.4", 6845 + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", 6846 + "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==", 6847 + "funding": { 6848 + "type": "github", 6849 + "url": "https://github.com/sponsors/dcastil" 6840 6850 } 6841 6851 }, 6842 6852 "node_modules/tailwindcss": {
+1
package.json
··· 21 21 "astro": "^4.16.7", 22 22 "astro-og-canvas": "^0.5.4", 23 23 "svelte": "^5.1.1", 24 + "tailwind-merge": "^2.5.4", 24 25 "tailwindcss": "^3.4.14", 25 26 "typescript": "^5.6.3" 26 27 },
+7
src/utils.ts
··· 1 1 import { getCollection } from "astro:content"; 2 + import { type ClassValue, clsx } from 'clsx'; 3 + import { twMerge } from 'tailwind-merge'; 4 + 5 + export function cn(...inputs: ClassValue[]) { 6 + return twMerge(clsx(inputs)); 7 + } 8 + 2 9 3 10 export const getBlogPosts = async () => { 4 11 const posts = (await getCollection("blog"))
src/assets/earth.jpg

This is a binary file and will not be displayed.

+52
src/components/CodeCopyButton.astro
··· 1 + <script is:inline> 2 + document.addEventListener("DOMContentLoaded", () => { 3 + let copyButtonLabel = "copy"; 4 + let codeBlocks = Array.from(document.querySelectorAll("pre")); 5 + for (let codeBlock of codeBlocks) { 6 + let wrapper = document.createElement("div"); 7 + wrapper.style.position = "relative"; 8 + 9 + let copyButton = document.createElement("button"); 10 + copyButton.className = 11 + "opacity-50 sm:opacity-20 group-hover:opacity-100 transition-opacity duration-500 absolute top-2 right-2 text-xs bg-base-200 dark:bg-base-700 border border-base-400 dark:border-base-600 py-1 px-2 rounded-xl dark:hover:bg-base-600 hover:bg-base-300"; 12 + copyButton.innerHTML = copyButtonLabel; 13 + 14 + // Add class 'group' to codeBlock 15 + codeBlock.classList.add("group"); 16 + 17 + codeBlock.setAttribute("tabindex", "0"); 18 + codeBlock.appendChild(copyButton); 19 + 20 + // Wrap the codeBlock with the wrapper div 21 + let parent = codeBlock.parentNode; 22 + if (parent) { 23 + parent.insertBefore(wrapper, codeBlock); 24 + wrapper.appendChild(codeBlock); 25 + } 26 + 27 + copyButton.addEventListener("click", async () => { 28 + await copyCode(codeBlock, copyButton); 29 + }); 30 + } 31 + 32 + async function copyCode(block, button) { 33 + let code = block.querySelector("code"); 34 + if (!code) return; 35 + 36 + let text = code.innerText; 37 + 38 + await navigator.clipboard.writeText(text); 39 + 40 + let oldLabel = button.innerText; 41 + let oldClassName = button.className; 42 + button.className = 43 + "opacity-100 transition-opacity duration-500 absolute top-2 right-2 text-xs bg-green-200 dark:bg-green-900 border border-green-300 dark:border-green-700 text-green-800 dark:text-green-300 py-1 px-2 rounded-xl"; 44 + button.innerText = "copied!"; 45 + 46 + setTimeout(() => { 47 + button.innerText = oldLabel; 48 + button.className = oldClassName; 49 + }, 2000); 50 + } 51 + }); 52 + </script>
-51
src/components/CodeCopyButton.svelte
··· 1 - <script lang="ts"> 2 - import { onMount } from "svelte"; 3 - 4 - onMount(() => { 5 - let copyButtonLabel = "copy"; 6 - let codeBlocks = Array.from(document.querySelectorAll("pre")); 7 - for (let codeBlock of codeBlocks) { 8 - let wrapper = document.createElement("div"); 9 - wrapper.style.position = "relative"; 10 - 11 - let copyButton = document.createElement("button"); 12 - copyButton.className = 13 - "opacity-50 sm:opacity-20 group-hover:opacity-100 transition-opacity duration-500 absolute top-2 right-2 text-xs bg-base-200 dark:bg-base-700 border border-base-400 dark:border-base-600 py-1 px-2 rounded-xl dark:hover:bg-base-600 hover:bg-base-300"; 14 - copyButton.innerHTML = copyButtonLabel; 15 - 16 - // add class group to codeBlock 17 - codeBlock.classList.add("group"); 18 - 19 - codeBlock.setAttribute("tabindex", "0"); 20 - codeBlock.appendChild(copyButton); 21 - 22 - codeBlock.parentNode?.insertBefore(wrapper, codeBlock); 23 - wrapper.appendChild(codeBlock); 24 - 25 - copyButton.addEventListener("click", async () => { 26 - await copyCode(codeBlock, copyButton); 27 - }); 28 - } 29 - 30 - async function copyCode(block: HTMLElement, button: HTMLElement) { 31 - let code = block.querySelector("code"); 32 - if (!code) return; 33 - 34 - let text = code.innerText; 35 - 36 - await navigator.clipboard.writeText(text); 37 - 38 - let oldLabel = button.innerText; 39 - let oldClassName = button.className; 40 - button.className = 41 - "opacity-100 transition-opacity duration-500 absolute top-2 right-2 text-xs bg-green-200 dark:bg-green-900 border border-green-300 dark:border-green-700 text-green-800 dark:text-green-300 py-1 px-2 rounded-xl"; 42 - 43 - button.innerText = "copied!"; 44 - 45 - setTimeout(() => { 46 - button.innerText = oldLabel; 47 - button.className = oldClassName; 48 - }, 2000); 49 - } 50 - }); 51 - </script>
+6 -5
src/components/Header.astro
··· 5 5 SEARCH_ENABLED, 6 6 SITE_FAVICON, 7 7 } from "../config.json"; 8 - import ThemeToggle from "./ThemeToggle.svelte"; 9 - import Search from "./search/Search.svelte"; 8 + import ThemeToggle from "./ThemeToggle.astro"; 10 9 import HeaderLink from "./HeaderLink.astro"; 10 + import CodeCopyButton from "./CodeCopyButton.astro"; 11 + 11 12 import CommandPalette from "./search/CommandPalette.svelte"; 12 - import CodeCopyButton from "./CodeCopyButton.svelte"; 13 + import Search from "./search/Search.svelte"; 13 14 14 15 const { active } = Astro.props; 15 16 --- ··· 38 39 class="text-sm font-semibold leading-6">About</HeaderLink 39 40 > 40 41 41 - {MANUAL_DARK_MODE ? <ThemeToggle client:visible /> : null} 42 + {MANUAL_DARK_MODE ? <ThemeToggle /> : null} 42 43 43 44 {SEARCH_ENABLED ? <Search client:visible /> : null} 44 45 </div> ··· 46 47 </header> 47 48 48 49 <CommandPalette client:load /> 49 - <CodeCopyButton client:load /> 50 + <CodeCopyButton />
+6
src/components/Pagination.astro
··· 2 2 import { BASE } from "../config.json"; 3 3 import PaginationNumber from "./PaginationNumber.astro"; 4 4 5 + type Props = { 6 + total: number; 7 + current: number; 8 + tag?: string; 9 + }; 10 + 5 11 const { total, current, tag } = Astro.props; 6 12 7 13 function getVisiblePages(total: number, current: number): Array<number | null> {
+99
src/components/ThemeToggle.astro
··· 1 + --- 2 + import { cn } from "../utils"; 3 + import type { HTMLAttributes } from "astro/types"; 4 + 5 + export type Props = HTMLAttributes<"button">; 6 + 7 + const { class: className, ...props } = Astro.props; 8 + --- 9 + 10 + <button 11 + class={cn( 12 + "flex items-center justify-center text-base-950 hover:text-base-600 dark:text-base-50 dark:hover:text-base-400", 13 + className 14 + )} 15 + data-theme-toggle 16 + {...props} 17 + > 18 + <span class="sr-only">Theme Toggle</span> 19 + 20 + <!-- Dark Mode Icon --> 21 + <svg 22 + xmlns="http://www.w3.org/2000/svg" 23 + fill="none" 24 + viewBox="0 0 24 24" 25 + stroke-width="1.5" 26 + stroke="currentColor" 27 + class="w-6 h-6 hidden dark:block" 28 + > 29 + <path 30 + stroke-linecap="round" 31 + stroke-linejoin="round" 32 + d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" 33 + ></path> 34 + </svg> 35 + 36 + <!-- Light Mode Icon --> 37 + <svg 38 + xmlns="http://www.w3.org/2000/svg" 39 + fill="none" 40 + viewBox="0 0 24 24" 41 + stroke-width="1.5" 42 + stroke="currentColor" 43 + class="w-6 h-6 block dark:hidden" 44 + > 45 + <path 46 + stroke-linecap="round" 47 + stroke-linejoin="round" 48 + d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 49 + 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 50 + 1.591M5.25 12H3m4.227-4.773L5.636 51 + 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 52 + 017.5 0z" 53 + ></path> 54 + </svg> 55 + </button> 56 + 57 + <script> 58 + (() => { 59 + let darkMode = false; 60 + 61 + function setTheme(dark: boolean) { 62 + const root = document.documentElement; 63 + if (dark) { 64 + root.classList.add("dark"); 65 + } else { 66 + root.classList.remove("dark"); 67 + } 68 + } 69 + 70 + function toggleTheme() { 71 + darkMode = !darkMode; 72 + localStorage.setItem("darkMode", JSON.stringify(darkMode)); 73 + setTheme(darkMode); 74 + } 75 + 76 + // Initialize theme on page load 77 + const savedDarkMode = localStorage.getItem("darkMode"); 78 + if (savedDarkMode !== null) { 79 + darkMode = JSON.parse(savedDarkMode); 80 + } else { 81 + darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; 82 + } 83 + setTheme(darkMode); 84 + 85 + // Listen to system theme changes 86 + window 87 + .matchMedia("(prefers-color-scheme: dark)") 88 + .addEventListener("change", (e) => e.matches && toggleTheme()); 89 + 90 + window 91 + .matchMedia("(prefers-color-scheme: light)") 92 + .addEventListener("change", (e) => e.matches && toggleTheme()); 93 + 94 + // Add click event to toggle button 95 + document 96 + .querySelector("button[data-theme-toggle]") 97 + ?.addEventListener("click", toggleTheme); 98 + })(); 99 + </script>
-88
src/components/ThemeToggle.svelte
··· 1 - <script lang="ts"> 2 - import { onMount } from "svelte"; 3 - 4 - let darkMode = false; 5 - 6 - onMount(() => { 7 - // load from local storage 8 - const savedDarkMode = localStorage.getItem("darkMode"); 9 - if (savedDarkMode) { 10 - darkMode = JSON.parse(savedDarkMode); 11 - } else { 12 - // prefers color scheme? 13 - darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; 14 - } 15 - 16 - // remove local storage 17 - // localStorage.removeItem("darkMode"); 18 - setTheme(darkMode); 19 - 20 - // recommended method for newer browsers: specify event-type as first argument 21 - window 22 - .matchMedia("(prefers-color-scheme: dark)") 23 - .addEventListener("change", (e) => e.matches && toggleTheme()); 24 - 25 - window 26 - .matchMedia("(prefers-color-scheme: light)") 27 - .addEventListener("change", (e) => e.matches && toggleTheme()); 28 - }); 29 - 30 - function setTheme(dark: Boolean) { 31 - var root = document.getElementsByTagName("html")[0]; 32 - 33 - if (dark) { 34 - root.classList.add("dark"); 35 - } else { 36 - root.classList.remove("dark"); 37 - } 38 - } 39 - 40 - function toggleTheme() { 41 - darkMode = !darkMode; 42 - // save to local storage 43 - localStorage.setItem("darkMode", JSON.stringify(darkMode)); 44 - setTheme(darkMode); 45 - } 46 - 47 - let classes = ""; 48 - export { classes as class }; 49 - 50 - export let id = ""; 51 - </script> 52 - 53 - <button 54 - on:click={toggleTheme} 55 - class="flex items-center justify-center text-base-950 hover:text-base-600 dark:text-base-50 dark:hover:text-base-400 {classes}" 56 - {id} 57 - > 58 - <span class="sr-only">Theme Toggle</span> 59 - 60 - <svg 61 - xmlns="http://www.w3.org/2000/svg" 62 - fill="none" 63 - viewBox="0 0 24 24" 64 - stroke-width="1.5" 65 - stroke="currentColor" 66 - class="w-6 h-6 hidden dark:block" 67 - > 68 - <path 69 - stroke-linecap="round" 70 - stroke-linejoin="round" 71 - d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" 72 - /> 73 - </svg> 74 - <svg 75 - xmlns="http://www.w3.org/2000/svg" 76 - fill="none" 77 - viewBox="0 0 24 24" 78 - stroke-width="1.5" 79 - stroke="currentColor" 80 - class="w-6 h-6 block dark:hidden" 81 - > 82 - <path 83 - stroke-linecap="round" 84 - stroke-linejoin="round" 85 - d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" 86 - /> 87 - </svg> 88 - </button>
+2 -2
src/layouts/BaseLayout.astro
··· 49 49 </head> 50 50 51 51 <body 52 - class="bg-base-100 dark:bg-base-950 min-h-full flex mx-1 sm:mx-4 transition-all duration-150" 52 + class="bg-base-100 dark:bg-base-950 flex mx-1 sm:mx-4 transition-all duration-150" 53 53 > 54 54 <div 55 - class="max-w-6xl mx-auto rounded-xl my-1 sm:my-8 w-full min-h-screen bg-base-50 ring-1 ring-base-200 dark:bg-base-900 dark:ring-base-300/20" 55 + class="shadow-lg max-w-6xl mx-auto rounded-xl my-1 sm:my-8 w-full bg-base-50 ring-1 ring-base-200 dark:bg-base-900 dark:ring-base-300/20" 56 56 > 57 57 <div class="w-full"> 58 58 <slot />
+6 -30
src/layouts/BlogPost.astro
··· 1 1 --- 2 - import { colorBaseClasses, colorAccentClasses } from "src/colors"; 3 - import { ACCENT_COLOR, BASE_COLOR } from "../config.json"; 4 - 5 - const { includeSearch } = Astro.props; 2 + import ProseWrapper from "./ProseWrapper.astro"; 6 3 --- 7 4 8 - { 9 - includeSearch ? ( 10 - <main 11 - data-pagefind-body 12 - class={ 13 - "prose prose-img:rounded-xl dark:prose-invert prose-a:no-underline prose-inline-code:bg-base-100 dark:prose-inline-code:bg-base-800 prose-inline-code:p-1 prose-inline-code:rounded-md " + 14 - colorBaseClasses[BASE_COLOR] + 15 - " " + 16 - colorAccentClasses[ACCENT_COLOR] 17 - } 18 - > 19 - <slot /> 20 - </main> 21 - ) : ( 22 - <main 23 - class={ 24 - "prose prose-img:rounded-xl dark:prose-invert prose-a:no-underline prose-inline-code:bg-base-100 dark:prose-inline-code:bg-base-800 prose-inline-code:p-1 prose-inline-code:rounded-md " + 25 - colorBaseClasses[BASE_COLOR] + 26 - " " + 27 - colorAccentClasses[ACCENT_COLOR] 28 - } 29 - > 30 - <slot /> 31 - </main> 32 - ) 33 - } 5 + <ProseWrapper> 6 + <main data-pagefind-body> 7 + <slot /> 8 + </main> 9 + </ProseWrapper>
+40
src/layouts/PostList.astro
··· 1 + --- 2 + import { SITE_TITLE, SITE_DESCRIPTION } from "../config.json"; 3 + 4 + import Footer from "$components/Footer.astro"; 5 + import Header from "$components/Header.astro"; 6 + import BaseLayout from "$layouts/BaseLayout.astro"; 7 + import BlogEntry from "$components/BlogEntry.astro"; 8 + 9 + import Pagination from "$components/Pagination.astro"; 10 + import ProseWrapper from "./ProseWrapper.astro"; 11 + 12 + type Props = { 13 + posts: Array<any>; 14 + totalPages: number; 15 + currentPage: number; 16 + tag?: string; 17 + }; 18 + 19 + const { posts, totalPages, currentPage, tag } = Astro.props; 20 + --- 21 + 22 + <BaseLayout title={SITE_TITLE} description={SITE_DESCRIPTION}> 23 + <Header active="blog" /> 24 + <main class="max-w-2xl lg:max-w-3xl mx-auto mt-16 px-4"> 25 + <ProseWrapper> 26 + <slot /> 27 + </ProseWrapper> 28 + <div class="my-14 space-y-16 max-w-3xl"> 29 + {posts.map((post: any) => <BlogEntry {...post.data} slug={post.slug} />)} 30 + </div> 31 + 32 + { 33 + totalPages > 1 ? ( 34 + <Pagination current={currentPage} total={totalPages} {tag} /> 35 + ) : null 36 + } 37 + </main> 38 + 39 + <Footer /> 40 + </BaseLayout>
+16
src/layouts/ProseWrapper.astro
··· 1 + --- 2 + import { colorBaseClasses, colorAccentClasses } from "src/colors"; 3 + import { ACCENT_COLOR, BASE_COLOR } from "../config.json"; 4 + import { cn } from "../utils"; 5 + --- 6 + 7 + <div 8 + class={cn( 9 + "prose prose-img:rounded-xl dark:prose-invert prose-a:no-underline", 10 + "prose-inline-code:bg-base-100 dark:prose-inline-code:bg-base-800 prose-inline-code:p-1 prose-inline-code:rounded-md", 11 + colorBaseClasses[BASE_COLOR], 12 + colorAccentClasses[ACCENT_COLOR] 13 + )} 14 + > 15 + <slot /> 16 + </div>
+5 -27
src/pages/index.astro
··· 1 1 --- 2 - import { SITE_TITLE, SITE_DESCRIPTION, POSTS_PER_PAGE } from "../config.json"; 3 - 4 - import Footer from "$components/Footer.astro"; 5 - import Header from "$components/Header.astro"; 6 - import BaseLayout from "$layouts/BaseLayout.astro"; 7 - import BlogPost from "$layouts/BlogPost.astro"; 8 - import BlogEntry from "$components/BlogEntry.astro"; 2 + import { POSTS_PER_PAGE } from "../config.json"; 9 3 10 4 import { Content } from "$content/info/description.md"; 11 - import Pagination from "$components/Pagination.astro"; 12 5 import { getBlogPosts } from "src/utils"; 6 + import PostList from "$layouts/PostList.astro"; 13 7 14 8 const posts = (await getBlogPosts()).splice(0, POSTS_PER_PAGE); 15 9 16 10 const total = Math.ceil((await getBlogPosts()).length / POSTS_PER_PAGE); 17 11 --- 18 12 19 - <BaseLayout title={SITE_TITLE} description={SITE_DESCRIPTION}> 20 - <Header active="blog" /> 21 - <main class="max-w-2xl lg:max-w-3xl mx-auto mt-16 px-4"> 22 - <BlogPost> 23 - <Content /> 24 - <div class="my-14 space-y-16 max-w-3xl not-prose"> 25 - { 26 - posts.map((post: any) => ( 27 - <BlogEntry {...post.data} slug={post.slug} /> 28 - )) 29 - } 30 - </div> 31 - </BlogPost> 32 - 33 - {total > 1 ? <Pagination current={1} total={total} /> : null} 34 - </main> 35 - 36 - <Footer /> 37 - </BaseLayout> 13 + <PostList {posts} totalPages={total} currentPage={1}> 14 + <Content /> 15 + </PostList>
+41
src/components/alerts/ErrorAlert.astro
··· 1 + --- 2 + import { cn } from "../../utils"; 3 + 4 + interface Props { 5 + title: string; 6 + class?: string; 7 + } 8 + 9 + const { title, class: className } = Astro.props; 10 + --- 11 + 12 + <div 13 + class={cn( 14 + "rounded-xl bg-red-50 dark:bg-red-500/5 p-4 dark:ring-1 dark:ring-red-500/10 not-prose max-w-full", 15 + className 16 + )} 17 + > 18 + <div class="flex"> 19 + <div class="flex-shrink-0"> 20 + <svg 21 + class="h-5 w-5 text-red-500" 22 + viewBox="0 0 20 20" 23 + fill="currentColor" 24 + aria-hidden="true" 25 + > 26 + <path 27 + fill-rule="evenodd" 28 + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" 29 + clip-rule="evenodd"></path> 30 + </svg> 31 + </div> 32 + <div class="ml-3"> 33 + <h3 class="text-sm font-medium text-red-800 dark:text-red-500"> 34 + {title} 35 + </h3> 36 + <div class="mt-2 text-sm text-red-700 dark:text-red-400"> 37 + <slot /> 38 + </div> 39 + </div> 40 + </div> 41 + </div>
-29
src/components/alerts/ErrorAlert.svelte
··· 1 - <script lang="ts"> 2 - export let title: string; 3 - 4 - let classes: string; 5 - 6 - export { classes as class }; 7 - </script> 8 - 9 - <div class="rounded-xl bg-red-50 dark:bg-red-500/5 p-4 dark:ring-1 dark:ring-red-500/10 not-prose max-w-full {classes}"> 10 - <div class="flex"> 11 - <div class="flex-shrink-0"> 12 - <svg class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> 13 - <path 14 - fill-rule="evenodd" 15 - d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" 16 - clip-rule="evenodd" 17 - /> 18 - </svg> 19 - </div> 20 - <div class="ml-3"> 21 - <h3 class="text-sm font-medium text-red-800 dark:text-red-500"> 22 - {title} 23 - </h3> 24 - <div class="mt-2 text-sm text-red-700 dark:text-red-400"> 25 - <slot /> 26 - </div> 27 - </div> 28 - </div> 29 - </div>
+41
src/components/alerts/InfoAlert.astro
··· 1 + --- 2 + import { cn } from "../../utils"; 3 + 4 + interface Props { 5 + title: string; 6 + class?: string; 7 + } 8 + 9 + const { title, class: className } = Astro.props; 10 + --- 11 + 12 + <div 13 + class={cn( 14 + "rounded-xl bg-blue-50 dark:bg-blue-500/5 p-4 dark:ring-1 dark:ring-blue-500/10 not-prose max-w-full", 15 + className 16 + )} 17 + > 18 + <div class="flex"> 19 + <div class="flex-shrink-0"> 20 + <svg 21 + class="h-5 w-5 text-blue-500" 22 + viewBox="0 0 20 20" 23 + fill="currentColor" 24 + aria-hidden="true" 25 + > 26 + <path 27 + fill-rule="evenodd" 28 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" 29 + clip-rule="evenodd"></path> 30 + </svg> 31 + </div> 32 + <div class="ml-3"> 33 + <h3 class="text-sm font-medium text-blue-800 dark:text-blue-500"> 34 + {title} 35 + </h3> 36 + <div class="mt-2 text-sm text-blue-700 dark:text-blue-400"> 37 + <slot /> 38 + </div> 39 + </div> 40 + </div> 41 + </div>
-27
src/components/alerts/InfoAlert.svelte
··· 1 - <script lang="ts"> 2 - export let title: string; 3 - 4 - let classes: string; 5 - 6 - export { classes as class }; 7 - </script> 8 - 9 - <div class="rounded-xl bg-blue-50 dark:bg-blue-500/5 p-4 dark:ring-1 dark:ring-blue-500/10 not-prose max-w-full {classes}"> 10 - <div class="flex"> 11 - <div class="flex-shrink-0"> 12 - <svg class="h-5 w-5 text-blue-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> 13 - <path 14 - fill-rule="evenodd" 15 - d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" 16 - clip-rule="evenodd" 17 - /> 18 - </svg> 19 - </div> 20 - <div class="ml-3"> 21 - <h3 class="text-sm font-medium text-blue-800 dark:text-blue-500">{title}</h3> 22 - <div class="mt-2 text-sm text-blue-700 dark:text-blue-400"> 23 - <slot></slot> 24 - </div> 25 - </div> 26 - </div> 27 - </div>
+43
src/components/alerts/SuccessAlert.astro
··· 1 + --- 2 + import { cn } from "../../utils"; 3 + 4 + interface Props { 5 + title: string; 6 + class?: string; 7 + } 8 + 9 + const { title, class: className } = Astro.props; 10 + --- 11 + 12 + <div 13 + class={cn( 14 + "rounded-xl bg-green-50 dark:bg-green-500/5 p-4 dark:ring-1 dark:ring-green-500/10 not-prose max-w-full", 15 + className 16 + )} 17 + > 18 + <div class="flex"> 19 + <div class="flex-shrink-0"> 20 + <svg 21 + class="h-5 w-5 text-green-500" 22 + viewBox="0 0 20 20" 23 + fill="currentColor" 24 + aria-hidden="true" 25 + > 26 + <path 27 + fill-rule="evenodd" 28 + d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" 29 + clip-rule="evenodd"></path> 30 + </svg> 31 + </div> 32 + <div class="ml-3"> 33 + <h3 class="text-sm font-medium text-green-800 dark:text-green-500"> 34 + {title} 35 + </h3> 36 + <div class="mt-2 text-sm text-green-700 dark:text-green-400"> 37 + <p> 38 + <slot /> 39 + </p> 40 + </div> 41 + </div> 42 + </div> 43 + </div>
-33
src/components/alerts/SuccessAlert.svelte
··· 1 - <script lang="ts"> 2 - export let title: string; 3 - 4 - let classes: string; 5 - 6 - export { classes as class }; 7 - </script> 8 - <div class="rounded-xl bg-green-50 dark:bg-green-500/5 p-4 dark:ring-1 dark:ring-green-500/10 not-prose max-w-full {classes}"> 9 - <div class="flex"> 10 - <div class="flex-shrink-0"> 11 - <svg 12 - class="h-5 w-5 text-green-500" 13 - viewBox="0 0 20 20" 14 - fill="currentColor" 15 - aria-hidden="true" 16 - > 17 - <path 18 - fill-rule="evenodd" 19 - d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" 20 - clip-rule="evenodd" 21 - /> 22 - </svg> 23 - </div> 24 - <div class="ml-3"> 25 - <h3 class="text-sm font-medium text-green-800 dark:text-green-500">{title}</h3> 26 - <div class="mt-2 text-sm text-green-700 dark:text-green-400"> 27 - <p> 28 - <slot></slot> 29 - </p> 30 - </div> 31 - </div> 32 - </div> 33 - </div>
+43
src/components/alerts/WarningAlert.astro
··· 1 + --- 2 + import { cn } from "../../utils"; 3 + 4 + interface Props { 5 + title: string; 6 + class?: string; 7 + } 8 + 9 + const { title, class: className } = Astro.props; 10 + --- 11 + 12 + <div 13 + class={cn( 14 + "rounded-xl bg-amber-50 dark:bg-amber-500/5 p-4 dark:ring-1 dark:ring-amber-500/10 not-prose max-w-full", 15 + className 16 + )} 17 + > 18 + <div class="flex"> 19 + <div class="flex-shrink-0"> 20 + <svg 21 + class="h-5 w-5 text-amber-500" 22 + viewBox="0 0 20 20" 23 + fill="currentColor" 24 + aria-hidden="true" 25 + > 26 + <path 27 + fill-rule="evenodd" 28 + d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" 29 + clip-rule="evenodd"></path> 30 + </svg> 31 + </div> 32 + <div class="ml-3"> 33 + <h3 class="text-sm font-medium text-amber-800 dark:text-amber-400"> 34 + {title} 35 + </h3> 36 + <div class="mt-2 text-sm text-amber-700 dark:text-amber-300"> 37 + <p> 38 + <slot /> 39 + </p> 40 + </div> 41 + </div> 42 + </div> 43 + </div>
-33
src/components/alerts/WarningAlert.svelte
··· 1 - <script lang="ts"> 2 - export let title: string; 3 - let classes: string; 4 - 5 - export { classes as class }; 6 - </script> 7 - 8 - <div class="rounded-xl bg-amber-50 dark:bg-amber-500/5 p-4 dark:ring-1 dark:ring-amber-500/10 not-prose max-w-full {classes}"> 9 - <div class="flex"> 10 - <div class="flex-shrink-0"> 11 - <svg 12 - class="h-5 w-5 text-amber-500" 13 - viewBox="0 0 20 20" 14 - fill="currentColor" 15 - aria-hidden="true" 16 - > 17 - <path 18 - fill-rule="evenodd" 19 - d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" 20 - clip-rule="evenodd" 21 - /> 22 - </svg> 23 - </div> 24 - <div class="ml-3"> 25 - <h3 class="text-sm font-medium text-amber-800 dark:text-amber-400">{title}</h3> 26 - <div class="mt-2 text-sm text-amber-700 dark:text-amber-300"> 27 - <p> 28 - <slot></slot> 29 - </p> 30 - </div> 31 - </div> 32 - </div> 33 - </div>
+3 -1
src/components/search/CommandPalette.svelte
··· 1 1 <script lang="ts"> 2 + import { BASE } from "../../config.json"; 3 + 2 4 import { fade, slide } from "svelte/transition"; 3 5 import type { Pagefind } from "vite-plugin-pagefind/types"; 4 6 import { showSearch } from "./CommandPaletteStore"; ··· 67 69 try { 68 70 // @ts-ignore 69 71 pagefind = await import( 70 - import.meta.env.BASE_URL + "/pagefind/pagefind.js" 72 + BASE + "/pagefind/pagefind.js" 71 73 ); 72 74 } catch (error) { 73 75 console.error("Pagefind module not found, will retry after build");
+5 -33
src/pages/pages/[...index].astro
··· 1 1 --- 2 - import { 3 - SITE_TITLE, 4 - SITE_DESCRIPTION, 5 - POSTS_PER_PAGE, 6 - } from "../../config.json"; 7 - 8 - import Header from "$components/Header.astro"; 9 - import Footer from "$components/Footer.astro"; 10 - import BaseLayout from "$layouts/BaseLayout.astro"; 11 - import BlogEntry from "$components/BlogEntry.astro"; 12 - import Pagination from "$components/Pagination.astro"; 2 + import { POSTS_PER_PAGE } from "../../config.json"; 13 3 import { getBlogPosts } from "src/utils"; 14 - 15 - import BlogPost from "$layouts/BlogPost.astro"; 4 + import PostList from "$layouts/PostList.astro"; 16 5 17 6 export async function getStaticPaths() { 18 7 const posts = await getBlogPosts(); ··· 33 22 const total = Math.ceil((await getBlogPosts()).length / POSTS_PER_PAGE); 34 23 --- 35 24 36 - <BaseLayout title={SITE_TITLE} description={SITE_DESCRIPTION}> 37 - <Header active="blog" /> 38 - 39 - <main class="mx-auto max-w-2xl lg:max-w-3xl py-16 px-4"> 40 - <BlogPost> 41 - <h1>All blog posts</h1> 42 - <div class="my-14 space-y-16 max-w-3xl not-prose"> 43 - { 44 - posts.map((post: any) => ( 45 - <BlogEntry {...post.data} slug={post.slug} /> 46 - )) 47 - } 48 - </div> 49 - </BlogPost> 50 - 51 - {total > 1 ? <Pagination current={index} total={total} /> : null} 52 - </main> 53 - 54 - <Footer /> 55 - </BaseLayout> 25 + <PostList {posts} totalPages={total} currentPage={index}> 26 + <h1>All blog posts</h1> 27 + </PostList>
+1 -1
src/pages/posts/[...slug].astro
··· 44 44 /> 45 45 ) : null 46 46 } 47 - <BlogPost includeSearch={true}> 47 + <BlogPost> 48 48 <div class="flex mt-8 items-center gap-x-4 text-xs"> 49 49 <FormattedDate date={post.data.pubDate} /> 50 50 <div class="flex gap-x-2">
+15 -55
src/pages/tags/[...tag]/[...index].astro
··· 1 1 --- 2 - import { 3 - SITE_TITLE, 4 - SITE_DESCRIPTION, 5 - POSTS_PER_PAGE, 6 - ACCENT_COLOR, 7 - BASE_COLOR, 8 - } from "../../../config.json"; 2 + import { POSTS_PER_PAGE } from "../../../config.json"; 9 3 10 - import Header from "$components/Header.astro"; 11 - import Footer from "$components/Footer.astro"; 12 - import BaseLayout from "$layouts/BaseLayout.astro"; 13 - import BlogEntry from "$components/BlogEntry.astro"; 14 - import Pagination from "$components/Pagination.astro"; 15 - import { colorBaseClasses, colorAccentClasses } from "src/colors"; 16 4 import { getBlogPosts } from "src/utils"; 5 + import PostList from "$layouts/PostList.astro"; 17 6 18 7 export async function getStaticPaths() { 19 8 const posts = await getBlogPosts(); 20 9 const tags = new Map<string, number>(); 21 10 11 + // get number of posts for each tag 22 12 posts.forEach((post: any) => { 23 13 post.data.tags?.forEach((tag: string) => 24 14 tags.set(tag, (tags.get(tag) || 0) + 1) ··· 31 21 }[] = []; 32 22 33 23 Array.from(tags).forEach(([tag, count]) => { 34 - const sanitizedTag = tag.split(" ").join("-"); // Replace spaces with hyphens 24 + const sanitizedTag = tag.split(" ").join("-"); 35 25 const totalPages = Math.ceil(count / POSTS_PER_PAGE); 36 26 37 - // 1. Path without index (defaults to page 1) 38 27 paths.push({ 39 28 params: { tag: sanitizedTag }, 40 29 props: { tag, index: 1 }, 41 30 }); 42 31 43 - // 2. Path with index=1 (same as no index) 44 - paths.push({ 45 - params: { tag: sanitizedTag, index: "1" }, 46 - props: { tag, index: 1 }, 47 - }); 48 - 49 - // 3. Additional paginated paths (from page 2 onwards) 50 - for (let i = 2; i <= totalPages; i++) { 32 + for (let i = 1; i <= totalPages; i++) { 51 33 paths.push({ 52 34 params: { tag: sanitizedTag, index: i.toString() }, 53 35 props: { tag, index: i }, ··· 70 52 ); 71 53 --- 72 54 73 - <BaseLayout title={SITE_TITLE} description={SITE_DESCRIPTION}> 74 - <Header active="blog" /> 75 - 76 - <main class="mx-auto max-w-2xl lg:max-w-3xl py-16"> 77 - <div 78 - class={"prose dark:prose-invert text-base-900 dark:text-base-50 px-4 " + 79 - colorBaseClasses[BASE_COLOR] + 80 - " " + 81 - colorAccentClasses[ACCENT_COLOR]} 82 - > 83 - <h1 class="inline-flex gap-4 flex-col sm:flex-row sm:items-center"> 84 - Posts tagged with <div> 85 - <p 86 - class="not-prose bold text-xl rounded-full inline-block bg-base-200 px-4 py-1.5 text-base-800 dark:bg-base-800 dark:text-base-300 border border-base-300 dark:border-base-700" 87 - > 88 - {tag} 89 - </p> 90 - </div> 91 - </h1> 55 + <PostList {posts} totalPages={total} currentPage={index ?? 1} {tag}> 56 + <h1 class="inline-flex gap-4 flex-col sm:flex-row sm:items-center"> 57 + Posts tagged with <div> 58 + <p 59 + class="not-prose bold text-xl rounded-full inline-block bg-base-200 px-4 py-1.5 text-base-800 dark:bg-base-800 dark:text-base-300 border border-base-300 dark:border-base-700" 60 + > 61 + {tag} 62 + </p> 92 63 </div> 93 - <div class="my-14 space-y-16 max-w-3xl px-4"> 94 - {posts.map((post: any) => <BlogEntry {...post.data} slug={post.slug} />)} 95 - </div> 96 - 97 - { 98 - total > 1 ? ( 99 - <Pagination current={index ?? 1} total={total} tag={tag} /> 100 - ) : null 101 - } 102 - </main> 103 - 104 - <Footer /> 105 - </BaseLayout> 64 + </h1> 65 + </PostList>