refactor(all): complete rewrite
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.md linguist-vendored
|
||||||
|
*.mdx linguist-vendored
|
|
@ -1,25 +1,79 @@
|
||||||
import sitemap from "@astrojs/sitemap";
|
import { defineConfig } from 'astro/config'
|
||||||
import tailwind from "@astrojs/tailwind";
|
import mdx from '@astrojs/mdx'
|
||||||
import remarkCallout from "@r4ai/remark-callout";
|
import react from '@astrojs/react'
|
||||||
|
import sitemap from '@astrojs/sitemap'
|
||||||
|
import tailwind from '@astrojs/tailwind'
|
||||||
import umami from "@yeskunall/astro-umami";
|
import umami from "@yeskunall/astro-umami";
|
||||||
import { defineConfig } from "astro/config";
|
import icon from 'astro-icon'
|
||||||
|
import { transformerCopyButton } from '@rehype-pretty/transformers'
|
||||||
|
import {
|
||||||
|
transformerMetaHighlight,
|
||||||
|
transformerNotationDiff,
|
||||||
|
} from '@shikijs/transformers'
|
||||||
|
import { rehypeHeadingIds } from '@astrojs/markdown-remark'
|
||||||
|
import rehypeKatex from 'rehype-katex'
|
||||||
|
import rehypeExternalLinks from 'rehype-external-links'
|
||||||
|
import rehypePrettyCode from 'rehype-pretty-code'
|
||||||
|
import remarkCallout from "@r4ai/remark-callout";
|
||||||
|
import remarkEmoji from 'remark-emoji'
|
||||||
|
import remarkMath from 'remark-math'
|
||||||
|
import remarkToc from 'remark-toc'
|
||||||
|
import sectionize from '@hbsnow/rehype-sectionize'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://blog.z0x.ca",
|
site: 'https://blog.z0x.ca',
|
||||||
integrations: [
|
integrations: [
|
||||||
tailwind({
|
tailwind({
|
||||||
nesting: true,
|
applyBaseStyles: false,
|
||||||
}),
|
}),
|
||||||
|
sitemap(),
|
||||||
|
mdx(),
|
||||||
|
react(),
|
||||||
umami({
|
umami({
|
||||||
id: "b691181e-cad7-4c23-b16a-709872a0a7ab",
|
id: "b691181e-cad7-4c23-b16a-709872a0a7ab",
|
||||||
endpointUrl: "https://umami.z0x.ca",
|
endpointUrl: "https://umami.z0x.ca",
|
||||||
}),
|
}),
|
||||||
sitemap(),
|
icon(),
|
||||||
],
|
],
|
||||||
markdown: {
|
markdown: {
|
||||||
shikiConfig: {
|
syntaxHighlight: false,
|
||||||
theme: "css-variables",
|
rehypePlugins: [
|
||||||
|
[
|
||||||
|
rehypeExternalLinks,
|
||||||
|
{
|
||||||
|
target: '_blank',
|
||||||
|
rel: ['nofollow', 'noreferrer', 'noopener'],
|
||||||
},
|
},
|
||||||
remarkPlugins: [remarkCallout],
|
],
|
||||||
|
rehypeHeadingIds,
|
||||||
|
rehypeKatex,
|
||||||
|
sectionize,
|
||||||
|
[
|
||||||
|
rehypePrettyCode,
|
||||||
|
{
|
||||||
|
theme: {
|
||||||
|
light: 'github-light-high-contrast',
|
||||||
|
dark: 'github-dark-high-contrast',
|
||||||
},
|
},
|
||||||
});
|
transformers: [
|
||||||
|
transformerNotationDiff(),
|
||||||
|
transformerMetaHighlight(),
|
||||||
|
transformerCopyButton({
|
||||||
|
visibility: 'hover',
|
||||||
|
feedbackDuration: 1000,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
remarkPlugins: [remarkToc, remarkMath, remarkEmoji, remarkCallout],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 4321,
|
||||||
|
host: true,
|
||||||
|
},
|
||||||
|
devToolbar: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
43
biome.json
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": false,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": false
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": false,
|
|
||||||
"ignore": ["node_modules", "dist"]
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "tab"
|
|
||||||
},
|
|
||||||
"organizeImports": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "double"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"include": ["*.astro"],
|
|
||||||
"linter": {
|
|
||||||
"rules": {
|
|
||||||
"style": {
|
|
||||||
"useConst": "off",
|
|
||||||
"useImportType": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
BIN
bun.lockb
20
components.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/styles/global.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
39
package.json
|
@ -1,26 +1,55 @@
|
||||||
{
|
{
|
||||||
"name": "blog.z0x.ca",
|
"name": "blog.z0x.ca",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "1.2.4",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro check && astro build",
|
"build": "astro check && astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"prettier": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/markdown-remark": "^6.1.0",
|
||||||
|
"@astrojs/mdx": "^4.0.8",
|
||||||
|
"@astrojs/react": "^4.2.0",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/rss": "^4.0.11",
|
||||||
"@astrojs/sitemap": "^3.2.1",
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@astrojs/tailwind": "^5.1.5",
|
||||||
"@fontsource-variable/nunito": "^5.1.1",
|
"@hbsnow/rehype-sectionize": "^1.0.7",
|
||||||
|
"@iconify-json/lucide": "^1.2.25",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||||
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||||
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@rehype-pretty/transformers": "^0.13.2",
|
||||||
"@r4ai/remark-callout": "^0.6.2",
|
"@r4ai/remark-callout": "^0.6.2",
|
||||||
|
"@shikijs/transformers": "^1.29.2",
|
||||||
|
"@types/react": "^18.3.18",
|
||||||
|
"@types/react-dom": "^18.3.5",
|
||||||
"@yeskunall/astro-umami": "^0.0.3",
|
"@yeskunall/astro-umami": "^0.0.3",
|
||||||
"astro": "^5.2.5",
|
"astro": "^5.2.5",
|
||||||
|
"astro-icon": "^1.1.5",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.469.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"rehype-external-links": "^3.0.0",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"rehype-pretty-code": "^0.14.0",
|
||||||
|
"remark-emoji": "^5.0.1",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-toc": "^9.0.0",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -28,8 +57,6 @@
|
||||||
"@tailwindcss/typography": "^0.5.16"
|
"@tailwindcss/typography": "^0.5.16"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@biomejs/biome",
|
"@biomejs/biome"
|
||||||
"esbuild",
|
|
||||||
"sharp"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 358 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 673 B |
|
@ -26,7 +26,8 @@
|
||||||
fx="128.00018"
|
fx="128.00018"
|
||||||
fy="128.00395"
|
fy="128.00395"
|
||||||
r="73.343147"
|
r="73.343147"
|
||||||
gradientUnits="userSpaceOnUse" /></defs><rect
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(1.0225904,0,0,1.0225904,4.1082412,0.10439132)" /></defs><rect
|
||||||
style="fill:#141414;fill-opacity:1;stroke-width:1.96924;stroke-linejoin:round"
|
style="fill:#141414;fill-opacity:1;stroke-width:1.96924;stroke-linejoin:round"
|
||||||
id="bg"
|
id="bg"
|
||||||
width="256"
|
width="256"
|
||||||
|
@ -34,11 +35,9 @@
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
ry="16" /><path
|
ry="16" /><path
|
||||||
style="display:inline;fill:url(#radialGradient15);stroke-width:1.18365"
|
style="display:inline;fill:url(#radialGradient15);stroke-width:1.21039"
|
||||||
d="M 55.824663,125.40476 125.21246,55.832186 a 3.9865567,3.9865567 0.02025256 0 1 5.64732,0.002 l 69.31756,69.600498 a 4.0005896,4.0005896 90.112473 0 1 -0.0111,5.65726 l -69.34652,69.08498 a 4.0135339,4.0135339 0.01148544 0 1 -5.6664,-0.001 L 55.832634,131.06116 a 3.9993654,3.9993654 89.919265 0 1 -0.008,-5.6564 z M 125.77344,89.300192 89.179269,125.4342 a 3.9542778,3.9542778 90.033181 0 0 -0.0033,5.62424 l 36.551738,36.17581 a 4.0462516,4.0462516 0.03318674 0 0 5.68928,0.003 l 36.59418,-36.13401 a 3.9542766,3.9542766 90.033183 0 0 0.003,-5.62424 L 131.46272,89.303486 a 4.0462513,4.0462513 0.03317792 0 0 -5.68928,-0.0033 z"
|
d="M 61.194006,128.3421 132.1493,57.197849 a 4.0766146,4.0766146 0 0 1 5.7749,0.002 l 70.88347,71.172791 a 4.0909645,4.0909645 0 0 1 -0.0113,5.78506 l -70.91309,70.64564 a 4.1042012,4.1042012 0 0 1 -5.79441,-0.001 L 61.202157,134.12628 a 4.0897127,4.0897127 0 0 1 -0.0082,-5.78418 z M 132.72295,91.42191 95.302106,128.3722 a 4.0436065,4.0436065 0 0 0 -0.0034,5.75129 l 37.377454,36.99304 a 4.137658,4.137658 0 0 0 5.8178,0.003 l 37.42086,-36.95029 a 4.0436053,4.0436053 0 0 0 0.003,-5.7513 L 138.54076,91.425279 a 4.1376577,4.1376577 0 0 0 -5.81781,-0.0034 z"
|
||||||
id="blur"
|
id="blur" /><path
|
||||||
transform="matrix(1.0225904,0,0,1.0225904,4.1082412,0.10439132)" /><path
|
style="display:inline;fill:#0a0a0a;fill-opacity:1;stroke:none;stroke-width:1.21039;stroke-opacity:1"
|
||||||
style="display:inline;fill:#0a0a0a;fill-opacity:1;stroke:none;stroke-width:1.18365;stroke-opacity:1"
|
d="M 54.194007,125.34209 125.1493,54.197846 a 4.0766146,4.0766146 0 0 1 5.7749,0.002 l 70.88347,71.172794 a 4.0909645,4.0909645 0 0 1 -0.0113,5.78506 l -70.91309,70.64564 a 4.1042012,4.1042012 0 0 1 -5.7944,-0.001 L 54.202158,131.12627 a 4.0897127,4.0897127 0 0 1 -0.0082,-5.78418 z M 125.72296,88.421908 88.302107,125.3722 a 4.0436065,4.0436065 0 0 0 -0.0034,5.75129 l 37.377453,36.99304 a 4.137658,4.137658 0 0 0 5.81781,0.003 l 37.42085,-36.9503 a 4.0436053,4.0436053 0 0 0 0.003,-5.75129 L 131.54076,88.425276 a 4.1376577,4.1376577 0 0 0 -5.8178,-0.0034 z"
|
||||||
d="M 55.824663,125.40476 125.21246,55.832186 a 3.9865567,3.9865567 0.02025256 0 1 5.64732,0.002 l 69.31756,69.600498 a 4.0005896,4.0005896 90.112473 0 1 -0.0111,5.65726 l -69.34652,69.08498 a 4.0135339,4.0135339 0.01148544 0 1 -5.6664,-0.001 L 55.832634,131.06116 a 3.9993654,3.9993654 89.919265 0 1 -0.008,-5.6564 z M 125.77344,89.300192 89.179269,125.4342 a 3.9542778,3.9542778 90.033181 0 0 -0.0033,5.62424 l 36.551738,36.17581 a 4.0462516,4.0462516 0.03318674 0 0 5.68928,0.003 l 36.59418,-36.13401 a 3.9542766,3.9542766 90.033183 0 0 0.003,-5.62424 L 131.46272,89.303486 a 4.0462513,4.0462513 0.03317792 0 0 -5.68928,-0.0033 z"
|
id="diamond" /></svg>
|
||||||
id="diamond"
|
|
||||||
transform="matrix(1.0225904,0,0,1.0225904,-2.8917572,-2.895611)" /></svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
public/fonts/GeistMonoVF.woff2
Normal file
BIN
public/fonts/GeistVF.woff2
Normal file
BIN
public/static/1200x630.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
public/static/logo.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
16
public/static/logo.svg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_271_118)">
|
||||||
|
<rect width="512" height="74" fill="#CCCCCC"/>
|
||||||
|
<rect y="146" width="512" height="74" fill="#CCCCCC"/>
|
||||||
|
<rect y="292" width="512" height="74" fill="#CCCCCC"/>
|
||||||
|
<rect y="438" width="512" height="74" fill="#CCCCCC"/>
|
||||||
|
<rect y="74" width="72" height="72" fill="#CCCCCC"/>
|
||||||
|
<rect y="366" width="72" height="72" fill="#CCCCCC"/>
|
||||||
|
<rect x="440" y="220" width="72" height="72" fill="#CCCCCC"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_271_118">
|
||||||
|
<rect width="512" height="512" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 632 B |
BIN
public/static/twitter-card.png
Normal file
After Width: | Height: | Size: 124 KiB |
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
entry: CollectionEntry<"blog">;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { entry } = Astro.props as {
|
|
||||||
entry: CollectionEntry<"blog">;
|
|
||||||
};
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href={`/${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>
|
|
|
@ -1,23 +0,0 @@
|
||||||
<button
|
|
||||||
id="back-to-top"
|
|
||||||
class="group relative flex w-fit flex-nowrap rounded 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>
|
|
53
src/components/BlogCard.astro
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { formatDate, readingTime } from "@/lib/utils";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import Link from "./Link.astro";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: CollectionEntry<"blog">;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { entry } = Astro.props as {
|
||||||
|
entry: CollectionEntry<"blog">;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedDate = formatDate(entry.data.date);
|
||||||
|
const readTime = readingTime(entry.body!);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="not-prose rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
|
||||||
|
>
|
||||||
|
<Link href={`/${entry.id}`} class="flex flex-col gap-4 sm:flex-row">
|
||||||
|
{
|
||||||
|
entry.data.image && (
|
||||||
|
<div class="max-w-[200px] sm:flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src={entry.data.image}
|
||||||
|
alt={entry.data.title}
|
||||||
|
width={1200}
|
||||||
|
height={630}
|
||||||
|
class="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="mb-1 text-lg font-semibold">
|
||||||
|
{entry.data.title}
|
||||||
|
</h3>
|
||||||
|
<p class="mb-2 text-sm text-muted-foreground">
|
||||||
|
{entry.data.description}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mb-2 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span>{formattedDate}</span>
|
||||||
|
<Separator orientation="vertical" className="h-4" />
|
||||||
|
<span>{readTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
53
src/components/Breadcrumbs.astro
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from '@/components/ui/breadcrumb'
|
||||||
|
import { Icon } from 'astro-icon/components'
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
href?: string
|
||||||
|
label: string
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: BreadcrumbItem[]
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items, class: className } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<Breadcrumb className={className}>
|
||||||
|
<BreadcrumbList>
|
||||||
|
{
|
||||||
|
items.map((item, index) => (
|
||||||
|
<>
|
||||||
|
{index !== 0 && <BreadcrumbSeparator />}
|
||||||
|
<BreadcrumbItem>
|
||||||
|
{index === items.length - 1 ? (
|
||||||
|
<BreadcrumbPage>
|
||||||
|
<span class="flex items-center gap-x-2">
|
||||||
|
{item.icon && <Icon name={item.icon} class="size-4" />}
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</BreadcrumbPage>
|
||||||
|
) : (
|
||||||
|
<BreadcrumbLink href={item.href}>
|
||||||
|
<span class="flex items-center gap-x-2">
|
||||||
|
{item.icon && <Icon name={item.icon} class="size-4" />}
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</BreadcrumbLink>
|
||||||
|
)}
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
|
@ -1,44 +0,0 @@
|
||||||
---
|
|
||||||
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 = "🚨";
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class={`not-prose callout callout-${type}`}>
|
|
||||||
<span class="emoji pointer-events-none select-none">{emoji}</span>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.callout {
|
|
||||||
@apply relative my-4 flex rounded border border-orange-800 bg-orange-100 p-3 text-orange-950 dark:border-orange-200/20 dark:bg-orange-950/20 dark:text-orange-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji {
|
|
||||||
@apply pr-3 text-xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout-info {
|
|
||||||
@apply border-blue-800 bg-blue-100 text-blue-950 dark:border-blue-200/20 dark:bg-blue-950/20 dark:text-blue-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout-warning {
|
|
||||||
@apply border-yellow-800 bg-yellow-100 text-yellow-950 dark:border-yellow-200/20 dark:bg-yellow-950/20 dark:text-yellow-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout-error {
|
|
||||||
@apply border-red-800 bg-red-100 text-red-950 dark:border-red-200/20 dark:bg-red-950/20 dark:text-red-200;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1 +1,11 @@
|
||||||
<div class="mx-auto max-w-screen-sm px-3"><slot /></div>
|
---
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { class: className } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={cn('mx-auto max-w-screen-md px-4', className)}><slot /></div>
|
||||||
|
|
|
@ -1,113 +1,24 @@
|
||||||
---
|
---
|
||||||
import BackToTop from "@components/BackToTop.astro";
|
import Container from '@/components/Container.astro'
|
||||||
import Container from "@components/Container.astro";
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { SITE } from "@consts";
|
import SocialIcons from './SocialIcons.astro'
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer>
|
<footer class="py-4">
|
||||||
<Container>
|
<Container>
|
||||||
<div class="relative">
|
<div
|
||||||
<div class="absolute -top-12 right-0">
|
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:justify-between"
|
||||||
<BackToTop />
|
>
|
||||||
</div>
|
<div class="flex items-center gap-x-2">
|
||||||
</div>
|
<span class="text-center text-sm text-muted-foreground">
|
||||||
<div class="flex items-center justify-between">
|
© {new Date().getFullYear()} • z0x
|
||||||
<div>© {new Date().getFullYear()} • {SITE.TITLE}</div>
|
</span>
|
||||||
<div class="flex flex-wrap items-center gap-1.5">
|
<Separator orientation="vertical" className="h-4" />
|
||||||
<a
|
<p class="text-center text-sm text-muted-foreground">
|
||||||
aria-label="RSS"
|
All rights reserved.
|
||||||
href="/rss.xml"
|
</p>
|
||||||
target="_blank"
|
|
||||||
class="group flex size-9 items-center justify-center rounded 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 group-hover:dark:stroke-white dark:group-focus-visible:stroke-white"
|
|
||||||
>
|
|
||||||
<path d="M5 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
|
||||||
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
|
||||||
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
id="light-theme-button"
|
|
||||||
aria-label="Light theme"
|
|
||||||
class="group flex size-9 items-center justify-center rounded 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 group-hover:dark: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 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 group-hover:dark: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 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 group-hover:dark: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>
|
||||||
|
<SocialIcons />
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
interface Props {
|
|
||||||
date: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { date } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<time datetime={date.toISOString()}>
|
|
||||||
{
|
|
||||||
date.toLocaleDateString("en-SE", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</time>
|
|
|
@ -1,7 +1,9 @@
|
||||||
---
|
---
|
||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
import "../styles/callout.css";
|
import "../styles/callout.css";
|
||||||
import "@fontsource-variable/nunito";
|
|
||||||
|
import { SITE } from "@/consts";
|
||||||
|
import { ClientRouter } from "astro:transitions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -9,20 +11,35 @@ interface Props {
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description, image = "/blog-placeholder-1.jpg" } = Astro.props;
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||||
|
|
||||||
|
const { title, description, image = "/static/twitter-card.png" } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css"
|
||||||
|
integrity="sha384-zh0CIslj+VczCZtlzBcjt5ppRcsAmDnRem7ESsYwWwg3m/OaJ2l4x7YBZl9Kxxib"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="title" content={title} />
|
<meta name="title" content={title} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content={Astro.url} />
|
<meta property="og:url" content={Astro.url} />
|
||||||
|
<meta property="og:site_name" content={SITE.TITLE} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:image" content={new URL(image, Astro.url)} />
|
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||||
|
@ -33,172 +50,39 @@ const { title, description, image = "/blog-placeholder-1.jpg" } = Astro.props;
|
||||||
<meta property="twitter:description" content={description} />
|
<meta property="twitter:description" content={description} />
|
||||||
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
||||||
|
|
||||||
|
<ClientRouter />
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
function init() {
|
function setDarkMode(document) {
|
||||||
preloadTheme();
|
const getThemePreference = () => {
|
||||||
onScroll();
|
if (
|
||||||
updateThemeButtons();
|
typeof localStorage !== "undefined" &&
|
||||||
addCopyCodeButtons();
|
localStorage.getItem("theme")
|
||||||
|
) {
|
||||||
|
return localStorage.getItem("theme");
|
||||||
|
}
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "theme-light";
|
||||||
|
};
|
||||||
|
const isDark = getThemePreference() === "dark";
|
||||||
|
document.documentElement.classList[isDark ? "add" : "remove"]("dark");
|
||||||
|
|
||||||
const backToTop = document.getElementById("back-to-top");
|
if (typeof localStorage !== "undefined") {
|
||||||
backToTop?.addEventListener("click", (event) => scrollToTop(event));
|
const observer = new MutationObserver(() => {
|
||||||
|
const isDark = document.documentElement.classList.contains("dark");
|
||||||
const backToPrev = document.getElementById("back-to-prev");
|
localStorage.setItem("theme", isDark ? "dark" : "theme-light");
|
||||||
backToPrev?.addEventListener("click", () => window.history.back());
|
|
||||||
|
|
||||||
const lightThemeButton = document.getElementById("light-theme-button");
|
|
||||||
lightThemeButton?.addEventListener("click", () => {
|
|
||||||
localStorage.setItem("theme", "light");
|
|
||||||
toggleTheme(false);
|
|
||||||
updateThemeButtons();
|
|
||||||
});
|
});
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
const darkThemeButton = document.getElementById("dark-theme-button");
|
attributes: true,
|
||||||
darkThemeButton?.addEventListener("click", () => {
|
attributeFilter: ["class"],
|
||||||
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 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => init());
|
setDarkMode(document);
|
||||||
document.addEventListener("astro:after-swap", () => init());
|
|
||||||
preloadTheme();
|
document.addEventListener("astro:before-swap", (ev) => {
|
||||||
|
setDarkMode(ev.newDocument);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
25
src/components/Header.astro
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
import Container from "@/components/Container.astro";
|
||||||
|
import Link from "@/components/Link.astro";
|
||||||
|
import { ModeToggle } from "@/components/ui/mode-toggle";
|
||||||
|
import { SITE } from "@/consts";
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 bg-background/50 backdrop-blur-md"
|
||||||
|
transition:persist
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4 py-4">
|
||||||
|
<Link
|
||||||
|
href="https://z0x.ca"
|
||||||
|
class="flex flex-shrink-0 items-center gap-2 text-xl font-semibold transition-colors duration-300 hover:text-primary"
|
||||||
|
>
|
||||||
|
{SITE.TITLE}
|
||||||
|
</Link>
|
||||||
|
<div class="flex items-center gap-2 md:gap-4">
|
||||||
|
<ModeToggle client:load transition:persist />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</header>
|
|
@ -1,29 +1,25 @@
|
||||||
---
|
---
|
||||||
import { cn } from "@lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
href: string;
|
href: string
|
||||||
external?: boolean;
|
external?: boolean
|
||||||
underline?: boolean;
|
class?: string
|
||||||
group?: boolean;
|
underline?: boolean
|
||||||
};
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const { href, external, class: className, underline, ...rest } = Astro.props
|
||||||
href,
|
|
||||||
external,
|
|
||||||
underline = true,
|
|
||||||
group = false,
|
|
||||||
...rest
|
|
||||||
} = Astro.props;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
target={external ? "_blank" : "_self"}
|
target={external ? '_blank' : '_self'}
|
||||||
class={cn(
|
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",
|
'inline-block transition-colors duration-300 ease-in-out',
|
||||||
underline && "underline underline-offset-[3px]",
|
underline &&
|
||||||
group && "group"
|
'underline decoration-muted-foreground underline-offset-[3px] hover:decoration-foreground',
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,33 +1,56 @@
|
||||||
---
|
---
|
||||||
const { prevPost, nextPost } = Astro.props;
|
import Link from '@/components/Link.astro'
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Icon } from 'astro-icon/components'
|
||||||
|
|
||||||
|
const { prevPost, nextPost } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-1.5 sm:gap-3">
|
<div class="col-start-2 flex flex-col gap-4 sm:flex-row">
|
||||||
{
|
<Link
|
||||||
prevPost?.id ? (
|
href={nextPost ? `/${nextPost.id}` : '#'}
|
||||||
<a href={`/${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">
|
class={cn(
|
||||||
<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">
|
buttonVariants({ variant: 'outline' }),
|
||||||
<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" />
|
'rounded-xl group flex items-center justify-start w-full sm:w-1/2 h-fit',
|
||||||
<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" />
|
!nextPost && 'pointer-events-none opacity-50 cursor-not-allowed',
|
||||||
</svg>
|
)}
|
||||||
<div class="flex items-center text-sm">{prevPost?.data.title}</div>
|
aria-disabled={!nextPost}
|
||||||
</a>
|
>
|
||||||
) : (
|
<div class="mr-2 flex-shrink-0">
|
||||||
<div class="invisible" />
|
<Icon
|
||||||
)
|
name="lucide:arrow-left"
|
||||||
}
|
class="size-4 transition-transform group-hover:-translate-x-1"
|
||||||
|
/>
|
||||||
{
|
</div>
|
||||||
nextPost?.id ? (
|
<div class="flex flex-col items-start overflow-hidden">
|
||||||
<a href={`/${nextPost?.id}`} class="group relative flex 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">
|
<span class="text-left text-xs text-muted-foreground">Next Post</span>
|
||||||
<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">
|
<span class="w-full truncate text-left text-sm"
|
||||||
<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" />
|
>{nextPost?.data.title || 'Latest post!'}</span
|
||||||
<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>
|
||||||
<div class="flex items-center text-sm">{nextPost?.data.title}</div>
|
</Link>
|
||||||
</a>
|
<Link
|
||||||
) : (
|
href={prevPost ? `/${prevPost.id}` : '#'}
|
||||||
<div class="invisible" />
|
class={cn(
|
||||||
)
|
buttonVariants({ variant: 'outline' }),
|
||||||
}
|
'rounded-xl group flex items-center justify-end w-full sm:w-1/2 h-fit',
|
||||||
|
!prevPost && 'pointer-events-none opacity-50 cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
aria-disabled={!prevPost}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-end overflow-hidden">
|
||||||
|
<span class="text-right text-xs text-muted-foreground">Previous Post</span
|
||||||
|
>
|
||||||
|
<span class="w-full truncate text-right text-sm"
|
||||||
|
>{prevPost?.data.title || 'Last post!'}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 flex-shrink-0">
|
||||||
|
<Icon
|
||||||
|
name="lucide:arrow-right"
|
||||||
|
class="size-4 transition-transform group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
19
src/components/SocialIcons.astro
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
import Link from '@/components/Link.astro'
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
import { Icon } from 'astro-icon/components'
|
||||||
|
---
|
||||||
|
|
||||||
|
<ul class="not-prose flex flex-wrap gap-2" role="list">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/rss.xml"
|
||||||
|
aria-label="RSS"
|
||||||
|
title="RSS"
|
||||||
|
class={buttonVariants({ variant: 'outline', size: 'icon' })}
|
||||||
|
external
|
||||||
|
>
|
||||||
|
<Icon name="lucide:rss" class="size-4" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
|
@ -1,50 +1,117 @@
|
||||||
---
|
---
|
||||||
import TableOfContentsHeading from "./TableOfContentsHeading.astro";
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Icon } from 'astro-icon/components'
|
||||||
const { headings } = Astro.props;
|
import TableOfContentsHeading from './TableOfContentsHeading.astro'
|
||||||
const toc = buildToc(headings);
|
|
||||||
|
|
||||||
export interface Heading {
|
export interface Heading {
|
||||||
depth: number;
|
depth: number
|
||||||
slug: string;
|
slug: string
|
||||||
text: string;
|
text: string
|
||||||
|
subheadings: Heading[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildToc(headings: Heading[]) {
|
const { headings } = Astro.props
|
||||||
const toc: Heading[] = [];
|
const toc = buildToc(headings)
|
||||||
const parentHeadings = new Map();
|
|
||||||
|
function buildToc(headings: Heading[]): Heading[] {
|
||||||
|
const toc: Heading[] = []
|
||||||
|
const stack: Heading[] = []
|
||||||
|
|
||||||
headings.forEach((h) => {
|
headings.forEach((h) => {
|
||||||
const heading = { ...h, subheadings: [] };
|
const heading = { ...h, subheadings: [] }
|
||||||
parentHeadings.set(heading.depth, heading);
|
|
||||||
if (heading.depth === 2) {
|
while (stack.length > 0 && stack[stack.length - 1].depth >= heading.depth) {
|
||||||
toc.push(heading);
|
stack.pop()
|
||||||
} else {
|
|
||||||
parentHeadings.get(heading.depth - 1).subheadings.push(heading);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return toc;
|
if (stack.length === 0) {
|
||||||
|
toc.push(heading)
|
||||||
|
} else {
|
||||||
|
stack[stack.length - 1].subheadings.push(heading)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.push(heading)
|
||||||
|
})
|
||||||
|
|
||||||
|
return toc
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<details open class="rounded-lg border border-black/15 dark:border-white/20">
|
<details
|
||||||
<summary>Table of Contents</summary>
|
open
|
||||||
<nav class="">
|
class="group col-start-2 mx-4 block rounded-xl border p-4 xl:hidden"
|
||||||
<ul class="py-3">
|
>
|
||||||
|
<summary
|
||||||
|
class="flex cursor-pointer items-center justify-between text-xl font-semibold group-open:pb-4"
|
||||||
|
>
|
||||||
|
Table of Contents
|
||||||
|
<Icon
|
||||||
|
name="lucide:chevron-down"
|
||||||
|
class="size-5 transition-transform group-open:rotate-180"
|
||||||
|
/>
|
||||||
|
</summary>
|
||||||
|
<ScrollArea
|
||||||
|
client:load
|
||||||
|
className="flex max-h-64 flex-col overflow-y-auto"
|
||||||
|
type="always"
|
||||||
|
>
|
||||||
|
<nav>
|
||||||
|
<ul></ul>
|
||||||
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
</ScrollArea>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<style>
|
<nav
|
||||||
summary {
|
class="sticky top-[5.5rem] col-start-1 hidden h-[calc(100vh-5.5rem)] text-xs leading-4 xl:block"
|
||||||
@apply cursor-pointer rounded-t-lg px-3 py-1.5 font-medium transition-colors;
|
>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<ScrollArea client:load className="max-h-[calc(100vh-8rem)]" type="always">
|
||||||
|
<ul
|
||||||
|
class="flex flex-col justify-end gap-y-2 overflow-y-auto px-8"
|
||||||
|
id="toc-container"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<h2 class="mb-2 text-lg font-semibold">Table of Contents</h2>
|
||||||
|
</li>
|
||||||
|
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||||
|
</ul>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function setupToc() {
|
||||||
|
const header = document.querySelector('header')
|
||||||
|
const headerHeight = header ? header.offsetHeight : 0
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(sections) => {
|
||||||
|
sections.forEach((section) => {
|
||||||
|
const heading = section.target.querySelector('h2, h3, h4, h5, h6')
|
||||||
|
if (!heading) return
|
||||||
|
|
||||||
|
const id = heading.getAttribute('id')
|
||||||
|
const link = document.querySelector(
|
||||||
|
`#toc-container li a[href="#${id}"]`,
|
||||||
|
)
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
const addRemove = section.isIntersecting ? 'add' : 'remove'
|
||||||
|
link.classList[addRemove]('text-foreground')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: `-${headerHeight}px 0px 0px 0px`,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const sections = document.querySelectorAll('.prose section')
|
||||||
|
sections.forEach((section) => {
|
||||||
|
observer.observe(section)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
summary:hover {
|
document.addEventListener('astro:page-load', setupToc)
|
||||||
@apply bg-black/5 dark:bg-white/5;
|
document.addEventListener('astro:after-swap', setupToc)
|
||||||
}
|
</script>
|
||||||
|
|
||||||
details[open] summary {
|
|
||||||
@apply bg-black/5 dark:bg-white/5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
---
|
---
|
||||||
import type { Heading } from "./TableOfContents.astro";
|
import Link from './Link.astro'
|
||||||
import Link from "./Link.astro";
|
import type { Heading } from './TableOfContents.astro'
|
||||||
|
|
||||||
const { heading } = Astro.props;
|
const { heading } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<li class="list-inside list-disc px-6 py-1.5 text-sm">
|
<li
|
||||||
<Link href={"#" + heading.slug} underline>
|
class="list-inside list-disc px-4 text-sm text-foreground/60 xl:list-none xl:p-0"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={'#' + heading.slug}
|
||||||
|
class="underline decoration-transparent underline-offset-[3px] transition-colors duration-200 hover:decoration-inherit py-1 xl:py-0"
|
||||||
|
>
|
||||||
{heading.text}
|
{heading.text}
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{
|
||||||
heading.subheadings.length > 0 && (
|
heading.subheadings.length > 0 && (
|
||||||
<ul class="translate-x-3">
|
<ul class="translate-x-3 xl:ml-4 xl:mt-2 xl:flex xl:translate-x-0 xl:flex-col xl:gap-2">
|
||||||
{heading.subheadings.map((subheading: Heading) => (
|
{heading.subheadings.map((subheading: Heading) => (
|
||||||
<Astro.self heading={subheading} />
|
<Astro.self heading={subheading} />
|
||||||
))}
|
))}
|
||||||
|
|
114
src/components/ui/breadcrumb.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
const Breadcrumb = React.forwardRef<
|
||||||
|
HTMLElement,
|
||||||
|
React.ComponentPropsWithoutRef<'nav'> & {
|
||||||
|
separator?: React.ReactNode
|
||||||
|
}
|
||||||
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||||
|
Breadcrumb.displayName = 'Breadcrumb'
|
||||||
|
|
||||||
|
const BreadcrumbList = React.forwardRef<
|
||||||
|
HTMLOListElement,
|
||||||
|
React.ComponentPropsWithoutRef<'ol'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ol
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbList.displayName = 'BreadcrumbList'
|
||||||
|
|
||||||
|
const BreadcrumbItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentPropsWithoutRef<'li'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<li
|
||||||
|
ref={ref}
|
||||||
|
className={cn('inline-flex items-center gap-1.5', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbItem.displayName = 'BreadcrumbItem'
|
||||||
|
|
||||||
|
const BreadcrumbLink = React.forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
React.ComponentPropsWithoutRef<'a'> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
>(({ asChild, className, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'a'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
className={cn('transition-colors hover:text-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
BreadcrumbLink.displayName = 'BreadcrumbLink'
|
||||||
|
|
||||||
|
const BreadcrumbPage = React.forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
React.ComponentPropsWithoutRef<'span'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn('font-normal text-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
BreadcrumbPage.displayName = 'BreadcrumbPage'
|
||||||
|
|
||||||
|
const BreadcrumbSeparator = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'li'>) => (
|
||||||
|
<li
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn('[&>svg]:size-3.5', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRightIcon />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'
|
||||||
|
|
||||||
|
const BreadcrumbEllipsis = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'span'>) => (
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<DotsHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis'
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
}
|
54
src/components/ui/button.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import { type VariantProps, cva } from 'class-variance-authority'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-secondary/50',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive text-destructive-foreground over:bg-destructive/50',
|
||||||
|
outline: 'border border-input bg-background hover:bg-secondary/50',
|
||||||
|
secondary:
|
||||||
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2',
|
||||||
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
|
lg: 'h-10 rounded-md px-8',
|
||||||
|
icon: 'h-9 w-9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button'
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
202
src/components/ui/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
DotFilledIcon,
|
||||||
|
} from '@radix-ui/react-icons'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<DotFilledIcon className="size-4 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1.5 text-sm font-semibold',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
}
|
70
src/components/ui/mode-toggle.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Laptop, Moon, Sun } from 'lucide-react'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const [theme, setThemeState] = React.useState<
|
||||||
|
'theme-light' | 'dark' | 'system'
|
||||||
|
>('theme-light')
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const isDarkMode = document.documentElement.classList.contains('dark')
|
||||||
|
setThemeState(isDarkMode ? 'dark' : 'theme-light')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const isDark =
|
||||||
|
theme === 'dark' ||
|
||||||
|
(theme === 'system' &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
|
||||||
|
document.documentElement.classList.add('disable-transitions')
|
||||||
|
|
||||||
|
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
|
||||||
|
|
||||||
|
window
|
||||||
|
.getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue('opacity')
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.documentElement.classList.remove('disable-transitions')
|
||||||
|
})
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="group"
|
||||||
|
title="Toggle theme"
|
||||||
|
>
|
||||||
|
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="bg-background">
|
||||||
|
<DropdownMenuItem onClick={() => setThemeState('theme-light')}>
|
||||||
|
<Sun className="mr-2 size-4" />
|
||||||
|
<span>Light</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setThemeState('dark')}>
|
||||||
|
<Moon className="mr-2 size-4" />
|
||||||
|
<span>Dark</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setThemeState('system')}>
|
||||||
|
<Laptop className="mr-2 size-4" />
|
||||||
|
<span>System</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
174
src/components/ui/pagination.tsx
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { type ButtonProps, buttonVariants } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
className={cn('mx-auto flex w-full justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
Pagination.displayName = 'Pagination'
|
||||||
|
|
||||||
|
const PaginationContent = React.forwardRef<
|
||||||
|
HTMLUListElement,
|
||||||
|
React.ComponentProps<'ul'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ul
|
||||||
|
ref={ref}
|
||||||
|
className={cn('flex flex-row items-center gap-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
PaginationContent.displayName = 'PaginationContent'
|
||||||
|
|
||||||
|
const PaginationItem = React.forwardRef<
|
||||||
|
HTMLLIElement,
|
||||||
|
React.ComponentProps<'li'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<li ref={ref} className={cn('', className)} {...props} />
|
||||||
|
))
|
||||||
|
PaginationItem.displayName = 'PaginationItem'
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
|
} & Pick<ButtonProps, 'size'> &
|
||||||
|
React.ComponentProps<'a'>
|
||||||
|
|
||||||
|
const PaginationLink = ({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
isDisabled,
|
||||||
|
size = 'icon',
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) => (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? 'outline' : 'ghost',
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
isDisabled && 'pointer-events-none opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
PaginationLink.displayName = 'PaginationLink'
|
||||||
|
|
||||||
|
const PaginationPrevious = ({
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn('gap-1 pl-2.5', className)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
<span>Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
PaginationPrevious.displayName = 'PaginationPrevious'
|
||||||
|
|
||||||
|
const PaginationNext = ({
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn('gap-1 pr-2.5', className)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span>Next</span>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
PaginationNext.displayName = 'PaginationNext'
|
||||||
|
|
||||||
|
const PaginationEllipsis = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'span'>) => (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
PaginationEllipsis.displayName = 'PaginationEllipsis'
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number
|
||||||
|
totalPages: number
|
||||||
|
baseUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationComponent: React.FC<PaginationProps> = ({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
baseUrl,
|
||||||
|
}) => {
|
||||||
|
const pages = Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||||
|
|
||||||
|
const getPageUrl = (page: number) => {
|
||||||
|
if (page === 1) return baseUrl
|
||||||
|
return `${baseUrl}${page}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent className="flex-wrap">
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
href={currentPage > 1 ? getPageUrl(currentPage - 1) : undefined}
|
||||||
|
isDisabled={currentPage === 1}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{pages.map((page) => (
|
||||||
|
<PaginationItem key={page}>
|
||||||
|
<PaginationLink
|
||||||
|
href={getPageUrl(page)}
|
||||||
|
isActive={page === currentPage}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{totalPages > 5 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
href={
|
||||||
|
currentPage < totalPages ? getPageUrl(currentPage + 1) : undefined
|
||||||
|
}
|
||||||
|
isDisabled={currentPage === totalPages}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaginationComponent
|
46
src/components/ui/scroll-area.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative overflow-hidden', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
'flex touch-none select-none transition-colors',
|
||||||
|
orientation === 'vertical' &&
|
||||||
|
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||||
|
orientation === 'horizontal' &&
|
||||||
|
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
))
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
28
src/components/ui/separator.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = 'horizontal', decorative = true, ...props },
|
||||||
|
ref,
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 bg-border',
|
||||||
|
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Separator }
|
|
@ -1,8 +1,19 @@
|
||||||
import type { Metadata, Site } from "@types";
|
export type Site = {
|
||||||
|
TITLE: string
|
||||||
|
DESCRIPTION: string
|
||||||
|
POSTS_PER_PAGE: number
|
||||||
|
SITEURL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Link = {
|
||||||
|
href: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export const SITE: Site = {
|
export const SITE: Site = {
|
||||||
TITLE: "z0x",
|
TITLE: "z0x's blog",
|
||||||
DESCRIPTION: "z0x's blog",
|
DESCRIPTION: "z0x's blog",
|
||||||
};
|
POSTS_PER_PAGE: 3,
|
||||||
export const HOME: Metadata = {
|
SITEURL: 'https://astro-erudite.vercel.app',
|
||||||
TITLE: "blog",
|
}
|
||||||
};
|
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
import { defineCollection, z } from "astro:content";
|
import { glob } from 'astro/loaders'
|
||||||
import { glob } from "astro/loaders";
|
import { defineCollection, z } from 'astro:content'
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content" }),
|
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
|
||||||
schema: z.object({
|
schema: ({ image }) =>
|
||||||
title: z.string(),
|
z.object({
|
||||||
description: z.string(),
|
title: z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
60,
|
||||||
|
'Title should be 60 characters or less for optimal Open Graph display.',
|
||||||
|
),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
155,
|
||||||
|
'Description should be 155 characters or less for optimal Open Graph display.',
|
||||||
|
),
|
||||||
date: z.coerce.date(),
|
date: z.coerce.date(),
|
||||||
|
image: image().optional(),
|
||||||
draft: z.boolean().optional(),
|
draft: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
});
|
})
|
||||||
|
|
||||||
export const collections = { blog };
|
|
||||||
|
export const collections = { blog }
|
||||||
|
|
BIN
src/content/blog/2022-post/2022.png
Normal file
After Width: | Height: | Size: 90 KiB |
9
src/content/blog/2022-post/index.mdx
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
title: '2022 Post'
|
||||||
|
description: 'This a dummy post written in the year 2022.'
|
||||||
|
date: 2022-06-01
|
||||||
|
tags: ['dummy', 'placeholder']
|
||||||
|
image: './2022.png'
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a dummy post written in the year 2022.
|
BIN
src/content/blog/2023-post/2023.png
Normal file
After Width: | Height: | Size: 92 KiB |
9
src/content/blog/2023-post/index.mdx
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
title: '2023 Post'
|
||||||
|
description: 'This a dummy post written in the year 2023.'
|
||||||
|
date: 2023-06-01
|
||||||
|
tags: ['dummy', 'placeholder']
|
||||||
|
image: './2023.png'
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a dummy post written in the year 2023.
|
BIN
src/content/blog/2024-post/2024.png
Normal file
After Width: | Height: | Size: 93 KiB |
153
src/content/blog/2024-post/index.mdx
vendored
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
---
|
||||||
|
title: '2024 Post'
|
||||||
|
description: 'This a dummy post written in the year 2024 (with multiple authors).'
|
||||||
|
date: 2024-06-01
|
||||||
|
tags: ['dummy', 'placeholder']
|
||||||
|
image: './2024.png'
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a dummy post written in the year 2024! Here is a long blog post with heavily nested headers, which can be used to test the table of contents:
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
###### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
###### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
###### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
###### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
###### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
|
@ -3,6 +3,7 @@ title: "Artix Linux install guide"
|
||||||
description: "Guide to installing Artix Linux with OpenRC and full disk encryption for UEFI and BIOS systems."
|
description: "Guide to installing Artix Linux with OpenRC and full disk encryption for UEFI and BIOS systems."
|
||||||
date: "2025-01-07"
|
date: "2025-01-07"
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
@ -31,7 +32,7 @@ Use [Rufus](https://rufus.ie/en), here is a [guide](https://www.wikihow.com/Use-
|
||||||
2. Find the corresponding block device for the flash drive in `/dev` folder. Usually it is `/dev/sdb`.
|
2. Find the corresponding block device for the flash drive in `/dev` folder. Usually it is `/dev/sdb`.
|
||||||
3. Burn the image to the flash drive (assuming your flash drive is /dev/sdb and that your terminal is opened in the directory of the image)
|
3. Burn the image to the flash drive (assuming your flash drive is /dev/sdb and that your terminal is opened in the directory of the image)
|
||||||
|
|
||||||
```
|
```sh
|
||||||
sudo dd bs=4M if=./artix-base-openrc-YYYY.MM.DD-x86_64.iso of=/dev/sdb conv=fsync oflag=direct status=progress
|
sudo dd bs=4M if=./artix-base-openrc-YYYY.MM.DD-x86_64.iso of=/dev/sdb conv=fsync oflag=direct status=progress
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ Connect the computer via an Ethernet cable
|
||||||
> [!Note]
|
> [!Note]
|
||||||
>When encountering a code block as below throughout this guide, execute the commands within it directly in the terminal.
|
>When encountering a code block as below throughout this guide, execute the commands within it directly in the terminal.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
sudo rfkill unblock wifi
|
sudo rfkill unblock wifi
|
||||||
sudo ip link set wlan0 up
|
sudo ip link set wlan0 up
|
||||||
connmanctl
|
connmanctl
|
||||||
|
@ -74,7 +75,7 @@ connmanctl
|
||||||
> [!Tip]
|
> [!Tip]
|
||||||
>Network names can be tab-completed.
|
>Network names can be tab-completed.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
agent on
|
agent on
|
||||||
scan wifi
|
scan wifi
|
||||||
services
|
services
|
||||||
|
@ -83,7 +84,7 @@ services
|
||||||
> [!example]
|
> [!example]
|
||||||
>connect wifi_dc85de828967_38303944616e69656c73_managed_psk
|
>connect wifi_dc85de828967_38303944616e69656c73_managed_psk
|
||||||
|
|
||||||
```
|
```sh
|
||||||
connect {WiFi name}
|
connect {WiFi name}
|
||||||
quit
|
quit
|
||||||
```
|
```
|
||||||
|
@ -91,7 +92,7 @@ quit
|
||||||
### Verify internet connectivity
|
### Verify internet connectivity
|
||||||
|
|
||||||
Check for internet
|
Check for internet
|
||||||
```
|
```sh
|
||||||
ping artixlinux.org
|
ping artixlinux.org
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -100,7 +101,7 @@ ping artixlinux.org
|
||||||
## Update the system clock
|
## Update the system clock
|
||||||
|
|
||||||
Activate the NTP daemon to synchronize the computer's real-time clock:
|
Activate the NTP daemon to synchronize the computer's real-time clock:
|
||||||
```
|
```sh
|
||||||
rc-service ntpd start
|
rc-service ntpd start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ rc-service ntpd start
|
||||||
## Partition the disk
|
## Partition the disk
|
||||||
|
|
||||||
1. Install `gdisk`.
|
1. Install `gdisk`.
|
||||||
```
|
```sh
|
||||||
pacman -Sy gdisk
|
pacman -Sy gdisk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -119,17 +120,17 @@ pacman -Sy gdisk
|
||||||
> `nvme0n1` will be used as the target install drive throughout this guide, adapt it to your drive name.
|
> `nvme0n1` will be used as the target install drive throughout this guide, adapt it to your drive name.
|
||||||
> If you have an hdd, your drive will start with `sda`.
|
> If you have an hdd, your drive will start with `sda`.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
gdisk /dev/nvme0n1
|
gdisk /dev/nvme0n1
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Delete any existing partitions
|
2. Delete any existing partitions
|
||||||
```
|
```txt
|
||||||
Command (m for help): d
|
Command (m for help): d
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create a boot partition
|
3. Create a boot partition
|
||||||
```
|
```txt
|
||||||
Command (m for help): n
|
Command (m for help): n
|
||||||
Partition number (1-128, default 1):
|
Partition number (1-128, default 1):
|
||||||
First sector (...):
|
First sector (...):
|
||||||
|
@ -138,7 +139,7 @@ Hex code or GUID (...): ef00
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create a root partition
|
4. Create a root partition
|
||||||
```
|
```txt
|
||||||
Command (m for help): n
|
Command (m for help): n
|
||||||
Partition number (2-128, default 1):
|
Partition number (2-128, default 1):
|
||||||
First sector (...):
|
First sector (...):
|
||||||
|
@ -147,20 +148,20 @@ Hex code or GUID (...): 8300
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Save the changes
|
5. Save the changes
|
||||||
```
|
```txt
|
||||||
Command (m for help): w
|
Command (m for help): w
|
||||||
Do you want to proceed? (Y/N): y
|
Do you want to proceed? (Y/N): y
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Verify partitioning
|
6. Verify partitioning
|
||||||
```
|
```sh
|
||||||
lsblk
|
lsblk
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
>It should look something like this:
|
>It should look something like this:
|
||||||
|
|
||||||
```
|
```txt
|
||||||
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
|
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
|
||||||
nvme0n1 259:0 0 465,8G 0 disk
|
nvme0n1 259:0 0 465,8G 0 disk
|
||||||
├─nvme0n1p1 259:1 0 512M 0 part
|
├─nvme0n1p1 259:1 0 512M 0 part
|
||||||
|
@ -176,13 +177,13 @@ nvme0n1 259:0 0 465,8G 0 disk
|
||||||
> [!Tip]
|
> [!Tip]
|
||||||
>Make sure to enter a secure passphrase and to write it down in a secure place as you will not be able to change it later
|
>Make sure to enter a secure passphrase and to write it down in a secure place as you will not be able to change it later
|
||||||
|
|
||||||
```
|
```sh
|
||||||
cryptsetup luksFormat /dev/nvme0n1p2
|
cryptsetup luksFormat /dev/nvme0n1p2
|
||||||
Are you sure (Type `yes` in capital letters): YES
|
Are you sure (Type `yes` in capital letters): YES
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Open the encrypted partition
|
2. Open the encrypted partition
|
||||||
```
|
```sh
|
||||||
cryptsetup open /dev/nvme0n1p2 root
|
cryptsetup open /dev/nvme0n1p2 root
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -191,12 +192,12 @@ cryptsetup open /dev/nvme0n1p2 root
|
||||||
## Create file systems
|
## Create file systems
|
||||||
|
|
||||||
1. Create the boot file system
|
1. Create the boot file system
|
||||||
```
|
```sh
|
||||||
mkfs.fat -F32 /dev/nvme0n1p1
|
mkfs.fat -F32 /dev/nvme0n1p1
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Create the root file system
|
1. Create the root file system
|
||||||
```
|
```sh
|
||||||
mkfs.ext4 /dev/mapper/root
|
mkfs.ext4 /dev/mapper/root
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -205,24 +206,24 @@ mkfs.ext4 /dev/mapper/root
|
||||||
## Mount file systems
|
## Mount file systems
|
||||||
|
|
||||||
1. Mount the root file system
|
1. Mount the root file system
|
||||||
```
|
```sh
|
||||||
mount /dev/mapper/root /mnt
|
mount /dev/mapper/root /mnt
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Mount the boot file system
|
2. Mount the boot file system
|
||||||
```
|
```sh
|
||||||
mount -m /dev/nvme0n1p1 /mnt/boot
|
mount -m /dev/nvme0n1p1 /mnt/boot
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Verify mounting
|
3. Verify mounting
|
||||||
```
|
```sh
|
||||||
lsblk
|
lsblk
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
>It should look something like this:
|
>It should look something like this:
|
||||||
|
|
||||||
```
|
```txt
|
||||||
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
|
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
|
||||||
nvme0n1 259:0 0 465,8G 0 disk
|
nvme0n1 259:0 0 465,8G 0 disk
|
||||||
├─nvme0n1p1 259:1 0 512M 0 part /mnt/boot
|
├─nvme0n1p1 259:1 0 512M 0 part /mnt/boot
|
||||||
|
@ -236,7 +237,7 @@ nvme0n1 259:0 0 465,8G 0 disk
|
||||||
|
|
||||||
Install the base system, kernel, init system and other essential packages.
|
Install the base system, kernel, init system and other essential packages.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
basestrap /mnt base linux linux-firmware openrc elogind-openrc cryptsetup cryptsetup-openrc efibootmgr doas nano
|
basestrap /mnt base linux linux-firmware openrc elogind-openrc cryptsetup cryptsetup-openrc efibootmgr doas nano
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -247,7 +248,7 @@ basestrap /mnt base linux linux-firmware openrc elogind-openrc cryptsetup crypts
|
||||||
|
|
||||||
Install AMD CPU microcode updates
|
Install AMD CPU microcode updates
|
||||||
|
|
||||||
```
|
```sh
|
||||||
basestrap /mnt amd-ucode
|
basestrap /mnt amd-ucode
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -255,7 +256,7 @@ basestrap /mnt amd-ucode
|
||||||
|
|
||||||
Install Intel CPU microcode updates
|
Install Intel CPU microcode updates
|
||||||
|
|
||||||
```
|
```sh
|
||||||
basestrap /mnt intel-ucode
|
basestrap /mnt intel-ucode
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -263,7 +264,7 @@ basestrap /mnt intel-ucode
|
||||||
|
|
||||||
## Generate File System Table
|
## Generate File System Table
|
||||||
|
|
||||||
```
|
```sh
|
||||||
fstabgen -U /mnt >> /mnt/etc/fstab
|
fstabgen -U /mnt >> /mnt/etc/fstab
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -271,7 +272,7 @@ fstabgen -U /mnt >> /mnt/etc/fstab
|
||||||
|
|
||||||
## Switch to New Installation
|
## Switch to New Installation
|
||||||
|
|
||||||
```
|
```sh
|
||||||
artix-chroot /mnt bash
|
artix-chroot /mnt bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -279,7 +280,7 @@ artix-chroot /mnt bash
|
||||||
|
|
||||||
## Network stack
|
## Network stack
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pacman -S wpa_supplicant networkmanager networkmanager-openrc iwd iwd-openrc
|
pacman -S wpa_supplicant networkmanager networkmanager-openrc iwd iwd-openrc
|
||||||
rc-update add NetworkManager
|
rc-update add NetworkManager
|
||||||
rc-update add iwd
|
rc-update add iwd
|
||||||
|
@ -295,7 +296,7 @@ EOF
|
||||||
> [!Info]
|
> [!Info]
|
||||||
>MAC randomization can be used for increased privacy by not disclosing your real MAC address to the WiFi network.
|
>MAC randomization can be used for increased privacy by not disclosing your real MAC address to the WiFi network.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
cat << EOF >> /etc/NetworkManager/conf.d/00-macrandomize.conf
|
cat << EOF >> /etc/NetworkManager/conf.d/00-macrandomize.conf
|
||||||
[device-mac-randomization]
|
[device-mac-randomization]
|
||||||
wifi.scan-rand-mac-address=yes
|
wifi.scan-rand-mac-address=yes
|
||||||
|
@ -314,12 +315,12 @@ EOF
|
||||||
>Feel free to change `en_DK.UTF-8` to your preferred locale such as `en_US.UTF-8` or `en_GB.UTF-8`
|
>Feel free to change `en_DK.UTF-8` to your preferred locale such as `en_US.UTF-8` or `en_GB.UTF-8`
|
||||||
|
|
||||||
1. Uncomment `en_DK.UTF-8`
|
1. Uncomment `en_DK.UTF-8`
|
||||||
```
|
```sh
|
||||||
nano /etc/locale.gen
|
nano /etc/locale.gen
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Generate locales.
|
2. Generate locales.
|
||||||
```
|
```sh
|
||||||
locale-gen
|
locale-gen
|
||||||
echo 'LANG=en_DK.UTF-8' > /etc/locale.conf
|
echo 'LANG=en_DK.UTF-8' > /etc/locale.conf
|
||||||
```
|
```
|
||||||
|
@ -331,7 +332,7 @@ echo 'LANG=en_DK.UTF-8' > /etc/locale.conf
|
||||||
> [!example]
|
> [!example]
|
||||||
>`ln -sf /usr/share/zoneinfo/Asia/Dubai /etc/localtime`
|
>`ln -sf /usr/share/zoneinfo/Asia/Dubai /etc/localtime`
|
||||||
|
|
||||||
```
|
```sh
|
||||||
ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
|
ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -339,7 +340,7 @@ ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
|
||||||
|
|
||||||
## Set hardware clock from system clock
|
## Set hardware clock from system clock
|
||||||
|
|
||||||
```
|
```sh
|
||||||
hwclock --systohc
|
hwclock --systohc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -350,24 +351,24 @@ hwclock --systohc
|
||||||
> [!Note]
|
> [!Note]
|
||||||
>Change `artix` to your desired hostname in all of the following commands
|
>Change `artix` to your desired hostname in all of the following commands
|
||||||
|
|
||||||
```
|
```sh
|
||||||
echo 'artix' > /etc/hostname
|
echo 'artix' > /etc/hostname
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Edit `/etc/conf.d/hostname`
|
1. Edit `/etc/conf.d/hostname`
|
||||||
```
|
```sh
|
||||||
nano /etc/conf.d/hostname
|
nano /etc/conf.d/hostname
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Replace `hostname="localhost"` with `hostname="artix"`
|
2. Replace `hostname="localhost"` with `hostname="artix"`
|
||||||
|
|
||||||
3. Edit `/etc/hosts`
|
3. Edit `/etc/hosts`
|
||||||
```
|
```sh
|
||||||
nano /etc/hosts
|
nano /etc/hosts
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Add the following:
|
4. Add the following:
|
||||||
```
|
```conf
|
||||||
127.0.0.1 localhost
|
127.0.0.1 localhost
|
||||||
::1 localhost
|
::1 localhost
|
||||||
127.0.1.1 artix.localdomain artix
|
127.0.1.1 artix.localdomain artix
|
||||||
|
@ -378,7 +379,7 @@ nano /etc/hosts
|
||||||
## Initramfs
|
## Initramfs
|
||||||
|
|
||||||
1. Edit `/etc/mkinitcpio.conf`
|
1. Edit `/etc/mkinitcpio.conf`
|
||||||
```
|
```sh
|
||||||
nano /etc/mkinitcpio.conf
|
nano /etc/mkinitcpio.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -387,13 +388,13 @@ nano /etc/mkinitcpio.conf
|
||||||
> [!Note]
|
> [!Note]
|
||||||
>It should look something like this:
|
>It should look something like this:
|
||||||
|
|
||||||
```
|
```conf
|
||||||
HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck)
|
HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck)
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Run this
|
3. Run this
|
||||||
|
|
||||||
```
|
```sh
|
||||||
mkinitcpio -P
|
mkinitcpio -P
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
@ -401,7 +402,7 @@ mkinitcpio -P
|
||||||
## Add a user
|
## Add a user
|
||||||
|
|
||||||
1. Set the root password.
|
1. Set the root password.
|
||||||
```
|
```sh
|
||||||
passwd
|
passwd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -410,7 +411,7 @@ passwd
|
||||||
> [!Tip]
|
> [!Tip]
|
||||||
>Change `artix` to your desired username
|
>Change `artix` to your desired username
|
||||||
|
|
||||||
```
|
```sh
|
||||||
useradd -m artix
|
useradd -m artix
|
||||||
passwd artix
|
passwd artix
|
||||||
```
|
```
|
||||||
|
@ -420,19 +421,19 @@ passwd artix
|
||||||
## Configure doas
|
## Configure doas
|
||||||
|
|
||||||
1. Create the config file.
|
1. Create the config file.
|
||||||
```
|
```sh
|
||||||
touch /etc/doas.conf
|
touch /etc/doas.conf
|
||||||
chown -c root:root /etc/doas.conf
|
chown -c root:root /etc/doas.conf
|
||||||
chmod -c 0400 /etc/doas.conf
|
chmod -c 0400 /etc/doas.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Edit `/etc/doas.conf`
|
2. Edit `/etc/doas.conf`
|
||||||
```
|
```sh
|
||||||
nano /etc/doas.conf
|
nano /etc/doas.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Add the following:
|
3. Add the following:
|
||||||
```
|
```conf
|
||||||
permit artix as root
|
permit artix as root
|
||||||
permit nopass artix as root cmd pacman
|
permit nopass artix as root cmd pacman
|
||||||
```
|
```
|
||||||
|
@ -446,14 +447,14 @@ permit nopass artix as root cmd pacman
|
||||||
>If you see a bunch of files listed, use EFISTUB.
|
>If you see a bunch of files listed, use EFISTUB.
|
||||||
>If you do not see a bunch of files listed, your system does not support UEFI and you should use GRUB.
|
>If you do not see a bunch of files listed, your system does not support UEFI and you should use GRUB.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
ls /sys/firmware/efi/efivars
|
ls /sys/firmware/efi/efivars
|
||||||
```
|
```
|
||||||
|
|
||||||
### EFISTUB
|
### EFISTUB
|
||||||
|
|
||||||
1. Get the UUID of your root partition.
|
1. Get the UUID of your root partition.
|
||||||
```
|
```sh
|
||||||
blkid -s UUID -o value /dev/nvme0n1p2
|
blkid -s UUID -o value /dev/nvme0n1p2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -463,44 +464,44 @@ blkid -s UUID -o value /dev/nvme0n1p2
|
||||||
>Replace xxxx with the UUID that you obtained earlier.
|
>Replace xxxx with the UUID that you obtained earlier.
|
||||||
>Replace `intel-ucode.img` with `amd-ucode.img` if you have an AMD CPU
|
>Replace `intel-ucode.img` with `amd-ucode.img` if you have an AMD CPU
|
||||||
|
|
||||||
```
|
```sh
|
||||||
efibootmgr -c -d /dev/nvme0n1 -p 1 -l /vmlinuz-linux -L "Artix" -u "cryptdevice=UUID=xxxx:root root=/dev/mapper/root rw initrd=\intel-ucode.img initrd=\initramfs-linux.img loglevel=3 quiet"
|
efibootmgr -c -d /dev/nvme0n1 -p 1 -l /vmlinuz-linux -L "Artix" -u "cryptdevice=UUID=xxxx:root root=/dev/mapper/root rw initrd=\intel-ucode.img initrd=\initramfs-linux.img loglevel=3 quiet"
|
||||||
```
|
```
|
||||||
|
|
||||||
### GRUB
|
### GRUB
|
||||||
|
|
||||||
1. Install grub on your boot partition
|
1. Install grub on your boot partition
|
||||||
```
|
```sh
|
||||||
pacman -S grub
|
pacman -S grub
|
||||||
grub-install /dev/sda
|
grub-install /dev/sda
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Get the UUID of your root partition.
|
2. Get the UUID of your root partition.
|
||||||
```
|
```sh
|
||||||
blkid -s UUID -o value /dev/nvme0n1p2
|
blkid -s UUID -o value /dev/nvme0n1p2
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Edit `/etc/default/grub`
|
3. Edit `/etc/default/grub`
|
||||||
```
|
```sh
|
||||||
nano /etc/default/grub
|
nano /etc/default/grub
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Add the following to the `GRUB_CMDLINE_LINUX` line, where xxxx is the UUID that you obtained earlier.
|
4. Add the following to the `GRUB_CMDLINE_LINUX` line, where xxxx is the UUID that you obtained earlier.
|
||||||
```
|
```conf
|
||||||
cryptdevice=UUID=xxxx:root root=/dev/mapper/root
|
cryptdevice=UUID=xxxx:root root=/dev/mapper/root
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
>It should look something like this:
|
>It should look something like this:
|
||||||
|
|
||||||
```
|
```conf
|
||||||
GRUB_CMDLINE_LINUX="cryptdevice=UUID=550e8400-e29b-41d4-a716-446655440000:root root=/dev/mapper/root"
|
GRUB_CMDLINE_LINUX="cryptdevice=UUID=550e8400-e29b-41d4-a716-446655440000:root root=/dev/mapper/root"
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Uncomment `#GRUB_ENABLE_CRYPTODISK=y`
|
5. Uncomment `#GRUB_ENABLE_CRYPTODISK=y`
|
||||||
|
|
||||||
6. Generate the config file.
|
6. Generate the config file.
|
||||||
```
|
```sh
|
||||||
grub-mkconfig -o /boot/grub/grub.cfg
|
grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -509,7 +510,7 @@ grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
## Reboot
|
## Reboot
|
||||||
|
|
||||||
1. You can reboot and enter into your new installation.
|
1. You can reboot and enter into your new installation.
|
||||||
```
|
```sh
|
||||||
exit
|
exit
|
||||||
umount -R /mnt
|
umount -R /mnt
|
||||||
reboot now
|
reboot now
|
||||||
|
@ -529,26 +530,26 @@ Login using the credentials that you set, if you followed the example your usern
|
||||||
#### Add arch extra repository
|
#### Add arch extra repository
|
||||||
|
|
||||||
1. Install packages
|
1. Install packages
|
||||||
```
|
```sh
|
||||||
doas pacman -Syu artix-archlinux-support curl
|
doas pacman -Syu artix-archlinux-support curl
|
||||||
doas pacman-key --populate archlinux
|
doas pacman-key --populate archlinux
|
||||||
doas sh -c "curl https://archlinux.org/mirrorlist/all -o /etc/pacman.d/mirrorlist-arch"
|
doas sh -c "curl https://archlinux.org/mirrorlist/all -o /etc/pacman.d/mirrorlist-arch"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Edit `/etc/pacman.d/mirrorlist-arch`
|
2. Edit `/etc/pacman.d/mirrorlist-arch`
|
||||||
```
|
```sh
|
||||||
doas nano /etc/pacman.d/mirrorlist-arch
|
doas nano /etc/pacman.d/mirrorlist-arch
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Uncomment the first server entries under the worldwide section
|
3. Uncomment the first server entries under the worldwide section
|
||||||
|
|
||||||
4. Edit `/etc/pacman.conf`
|
4. Edit `/etc/pacman.conf`
|
||||||
```
|
```sh
|
||||||
doas nano /etc/pacman.conf
|
doas nano /etc/pacman.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Add the following to the bottom of the file
|
5. Add the following to the bottom of the file
|
||||||
```
|
```conf
|
||||||
##Arch
|
##Arch
|
||||||
[extra]
|
[extra]
|
||||||
Include = /etc/pacman.d/mirrorlist-arch
|
Include = /etc/pacman.d/mirrorlist-arch
|
||||||
|
@ -559,7 +560,7 @@ Include = /etc/pacman.d/mirrorlist-arch
|
||||||
|
|
||||||
#### Sort for fastest mirrors
|
#### Sort for fastest mirrors
|
||||||
|
|
||||||
```
|
```sh
|
||||||
doas pacman -Syu reflector pacman-contrib
|
doas pacman -Syu reflector pacman-contrib
|
||||||
doas reflector --verbose -p https -l 30 -f 5 --sort rate --save /etc/pacman.d/mirrorlist-arch
|
doas reflector --verbose -p https -l 30 -f 5 --sort rate --save /etc/pacman.d/mirrorlist-arch
|
||||||
doas sh -c "curl https://gitea.artixlinux.org/packages/artix-mirrorlist/raw/branch/master/mirrorlist -o /etc/pacman.d/mirrorlist.bak"
|
doas sh -c "curl https://gitea.artixlinux.org/packages/artix-mirrorlist/raw/branch/master/mirrorlist -o /etc/pacman.d/mirrorlist.bak"
|
||||||
|
@ -569,7 +570,7 @@ doas sh -c "rankmirrors -v -n 5 /etc/pacman.d/mirrorlist.bak > /etc/pacman.d/mir
|
||||||
### AUR
|
### AUR
|
||||||
#### Install paru
|
#### Install paru
|
||||||
|
|
||||||
```
|
```sh
|
||||||
doas pacman -S --needed base-devel
|
doas pacman -S --needed base-devel
|
||||||
git clone https://aur.archlinux.org/paru.git
|
git clone https://aur.archlinux.org/paru.git
|
||||||
cd paru
|
cd paru
|
||||||
|
@ -580,7 +581,7 @@ rm -rf paru
|
||||||
|
|
||||||
#### Replace sudo with doas
|
#### Replace sudo with doas
|
||||||
|
|
||||||
```
|
```sh
|
||||||
doas pacman -Rdd sudo
|
doas pacman -Rdd sudo
|
||||||
doas ln -s /usr/bin/doas /usr/bin/sudo
|
doas ln -s /usr/bin/doas /usr/bin/sudo
|
||||||
```
|
```
|
||||||
|
@ -589,7 +590,7 @@ doas ln -s /usr/bin/doas /usr/bin/sudo
|
||||||
|
|
||||||
Install and enable the powerprofiles daemon
|
Install and enable the powerprofiles daemon
|
||||||
|
|
||||||
```
|
```sh
|
||||||
doas pacman -S power-profiles-daemon power-profiles-daemon-openrc
|
doas pacman -S power-profiles-daemon power-profiles-daemon-openrc
|
||||||
doas rc-update add power-profiles-daemon
|
doas rc-update add power-profiles-daemon
|
||||||
doas rc-service power-profiles-daemon start
|
doas rc-service power-profiles-daemon start
|
||||||
|
@ -597,7 +598,7 @@ doas rc-service power-profiles-daemon start
|
||||||
|
|
||||||
### Add swap
|
### Add swap
|
||||||
|
|
||||||
```
|
```sh
|
||||||
doas fallocate -l 4G /swapfile
|
doas fallocate -l 4G /swapfile
|
||||||
doas chmod 600 /swapfile
|
doas chmod 600 /swapfile
|
||||||
doas mkswap /swapfile
|
doas mkswap /swapfile
|
||||||
|
@ -608,7 +609,7 @@ echo '/swapfile none swap sw 0 0' | doas tee -a /etc/fstab
|
||||||
|
|
||||||
### Auto-mount an external LUKS encrypted drive
|
### Auto-mount an external LUKS encrypted drive
|
||||||
|
|
||||||
```
|
```sh
|
||||||
doas fdisk /dev/sdb
|
doas fdisk /dev/sdb
|
||||||
>g, n, w
|
>g, n, w
|
||||||
|
|
BIN
src/content/blog/the-state-of-static-blogs/1200x630.png
Normal file
After Width: | Height: | Size: 77 KiB |
182
src/content/blog/the-state-of-static-blogs/index.mdx
vendored
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
---
|
||||||
|
title: 'The State of Static Blogs in 2024'
|
||||||
|
description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.'
|
||||||
|
date: 2024-07-25
|
||||||
|
tags: ['webdev', 'opinion']
|
||||||
|
image: './1200x630.png'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Hello! My name is enscribe ([jktrn](https://github.com/jktrn) on GitHub), and I'm a fullstack web developer who has been fiddling with blogging platforms for a couple of years now. I run a blog at [enscribe.dev](https://enscribe.dev), where I write about cybersecurity and the capture-the-flag (CTF) scene.
|
||||||
|
|
||||||
|
I have a lot of opinions about what makes a great blogging template. As a cumulative result of all the slop, bullshit, and outright terrible design decisions I've had to deal with working with various templates and frameworks, I bring you [astro-erudite](https://github.com/jktrn/astro-erudite), which should hopefully bring a better developer and user experience in terms of ease of use, customization, and performance.
|
||||||
|
|
||||||
|
astro-erudite is written in Astro, a framework hyperoptimized for static content such as blogs. Aesthetically, it is also designed to be as boring as possible while still maintaining maximum functionality, as to allow for the freedom of the developer (or the designer they hire) to make their blog uniquely their own. Within the codebase of this template I've included many nuances that, in my opinion (and there will be many, many opinions here), make the developer experience significantly more pleasant. I've also _excluded_ many features that, frankly, you don't need.
|
||||||
|
|
||||||
|
## Welcoming some DX features
|
||||||
|
|
||||||
|
This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:
|
||||||
|
|
||||||
|
- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers—however, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utility colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
|
||||||
|
- [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) is a plugin that automatically styles any content surrounded by an `<article>{:html}` tag in a way which makes it readable and blog-post-friendly. It does this via a `prose` class which you can wrap anything with to style the interior content. This is especially useful for HTML you don't control, e.g. a post rendered from Markdown. Although your control over the rendering is a bit less fine-grained, you're also already using Tailwind so this right has long been forsaken.
|
||||||
|
- [Shiki](https://github.com/shikijs/shiki) is a syntax highlighter for code blocks. Although Astro code blocks utilize Shiki under the hood, I've actually disabled the default code blocks in this template so that they don't collide with my preferred library [rehype-pretty-code](https://rehype-pretty.pages.dev), which is _also_ powered by Shiki but allows for line numbers, line highlighting, inline code syntax highlighting, and a transformers API for advanced customization such as manual `diff` visualization and line blurring. This library does not ship with any CSS, and it's up to you to style the code blocks and code block titles as you see fit. I've provided styles in `src/styles/global.css` within the `@layer components{:css}` directive if you wish to fiddle with them. The following code block is an example of how to style code blocks using rehype-pretty-code, and was generated with the following Markdown code:
|
||||||
|
|
||||||
|
````mdx
|
||||||
|
```css title="src/styles/global.css" caption="Styling code blocks using rehype-pretty-code (with a caption down here)" showLineNumbers{80} {10-12} /apply/ /components/
|
||||||
|
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
```css title="src/styles/global.css" caption="Styling code blocks using rehype-pretty-code (with a caption down here)" showLineNumbers{80} {10-12} /apply/ /components/
|
||||||
|
@layer components {
|
||||||
|
article {
|
||||||
|
@apply prose-headings:scroll-mt-20;
|
||||||
|
|
||||||
|
.katex-display {
|
||||||
|
@apply overflow-x-auto overflow-y-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Removes background from <mark> elements */
|
||||||
|
mark {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blanket syntax highlighting */
|
||||||
|
code[data-theme*=' '] {
|
||||||
|
span { /* [!code ++] */
|
||||||
|
color: var(--shiki-light); /* [!code ++] */
|
||||||
|
} /* [!code ++] */
|
||||||
|
|
||||||
|
.dark & span { /* [!code --] */
|
||||||
|
color: var(--shiki-dark); /* [!code --] */
|
||||||
|
} /* [!code --] */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When I added those two diff additions and deletions, I simply added `/* [!code ++] */` and `/* [!code --] */` to the lines I wanted to highlight. Just use the comment syntax of whatever language you're attempting to highlight.
|
||||||
|
|
||||||
|
- The `cn(){:ts}` function is a utility function which combines [clsx](https://www.npmjs.com/package/clsx) and [tailwind-merge](https://www.npmjs.com/package/tailwind-merge), two packages which allow painless conditional class addition and concatenation:
|
||||||
|
|
||||||
|
```tsx title="src/lib/utils.ts" caption="A utility function for class name concatenation" showLineNumbers
|
||||||
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This needs to be in every single template. This is an example of it being used in my `<Link>{:tsx}` component:
|
||||||
|
|
||||||
|
```astro showLineNumbers title="src/components/Link.astro" caption="A custom Link component with tailwind-merge and clsx" {16-20}
|
||||||
|
---
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const { href, external, class: className, underline, ...rest } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target={external ? '_blank' : '_self'}
|
||||||
|
class={cn(
|
||||||
|
'inline-block transition-colors duration-300 ease-in-out',
|
||||||
|
underline &&
|
||||||
|
'underline decoration-muted-foreground underline-offset-[3px] hover:decoration-foreground',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
We were able to, in a single helper function:
|
||||||
|
|
||||||
|
1. Concatenate whatever the user passed via the `class` prop to our base styles
|
||||||
|
2. Conditionally add an underline if the `underline` prop is true
|
||||||
|
|
||||||
|
Awesome!
|
||||||
|
|
||||||
|
## Welcoming some UX features
|
||||||
|
|
||||||
|
Within the blog itself (as in the layout, appearance, and navigation) are features that I believe are essential for a great user experience:
|
||||||
|
|
||||||
|
- Images are awesome and, by default, your blog post should have an image associated with it as part of the post's [Open Graph](https://ogp.me/) metadata. Since you can do whatever you want with the image, all of my dummy posts will have a placeholder image placed within their folder in `src/content/blog/`. Whenever you load into a blog post, splat in the middle will be the image associated with that post in its frontmatter.
|
||||||
|
- Theme selectors should be self-explanatory. I've added one on the top right of the header, which is also `sticky` and not `absolute` such that it doesn't ignore the document flow (and thus you won't have to add `mt-20` to the top of every single page).
|
||||||
|
- The table of contents of a post shouldn't be reduced to a `<details closed>{:html}` at the start of a blog post on desktop. You'd need to go to the top of the page to navigate through items. I've added a sticky `TableOfContents` component which always hangs out around the unused left side margin of a blog post. I also attached a very tiny client-side script using [`IntersectionObserver{:ts}`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to highlight all of the headings you're viewing within the TOC as you scroll through the page—it also will handle nested headings in that the parent heading of a visible child will still be highlighted even if off-screen (see the dummy [2024 Post](/blog/2024-post) for an example of this). I'll still use a collapsible `<details>{:html}` element for the table of contents on mobile though since obviously a table of contents on the side is unfeasible for small screens.
|
||||||
|
- Every page, except the homepage, will have a `<Breadcrumb>{:tsx}` component which shows you your current location in the site hierarchy. I don't see these often in blog templates even though they are so amazing for both discoverability (SEO and crawling) and user experience (the user always knows how "deep" they are in the site).
|
||||||
|
- You can specify multiple post authors via frontmatter. If this post author's id is found within the `Authors` collection, then it will render particular info from that author's frontmatter file, `[author-name].md` (e.g. avatar, link to profile). For example, the previous post (2024 Post) has two authors: "enscribe" and "jktrn", where "enscribe" is the only author with a custom avatar since "jktrn" is unregistered.
|
||||||
|
- Each author will have their own page, which lists all of their posts. If you're the only author throughout the entire blog then you can simply disregard all aspects regarding both inserting authors and the `Authors` collection.
|
||||||
|
- Each tag will also have their own page, which lists all of the posts under that tag!
|
||||||
|
- $\LaTeX$ is fully supported with [KaTeX](https://katex.org/):
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
To solve the cubic equation $t^3 + pt + q = 0$ (where the real numbers
|
||||||
|
$p, q$ satisfy ${4p^3 + 27q^2} > 0$) one can use Cardano's formula:
|
||||||
|
|
||||||
|
$$
|
||||||
|
\sqrt[{3}]{
|
||||||
|
-\frac{q}{2}
|
||||||
|
+\sqrt{\frac{q^2}{4} + {\frac{p^{3}}{27}}}
|
||||||
|
}+
|
||||||
|
\sqrt[{3}]{
|
||||||
|
-\frac{q}{2}
|
||||||
|
-\sqrt{\frac{q^2}{4} + {\frac{p^{3}}{27}}}
|
||||||
|
}
|
||||||
|
$$
|
||||||
|
|
||||||
|
For any $u_1, \dots, u_n \in \mathbb{C}$ and
|
||||||
|
$v_1, \dots, v_n \in \mathbb{C}$, the Cauchy–Bunyakovsky–Schwarz
|
||||||
|
inequality can be written as follows:
|
||||||
|
|
||||||
|
$$
|
||||||
|
\left| \sum_{k=1}^n {u_k \bar{v_k}} \right|^2
|
||||||
|
\leq
|
||||||
|
{
|
||||||
|
\left( \sum_{k=1}^n {|u_k|} \right)^2
|
||||||
|
\left( \sum_{k=1}^n {|v_k|} \right)^2
|
||||||
|
}
|
||||||
|
$$
|
||||||
|
|
||||||
|
Finally, the determinant of a Vandermonde matrix can be calculated
|
||||||
|
using the following expression:
|
||||||
|
|
||||||
|
$$
|
||||||
|
\begin{vmatrix}
|
||||||
|
1 & x_1 & x_1^2 & \dots & x_1^{n-1} \\
|
||||||
|
1 & x_2 & x_2^2 & \dots & x_2^{n-1} \\
|
||||||
|
1 & x_3 & x_3^2 & \dots & x_3^{n-1} \\
|
||||||
|
\vdots & \vdots & \vdots & \ddots & \vdots \\
|
||||||
|
1 & x_n & x_n^2 & \dots & x_n^{n-1} \\
|
||||||
|
\end{vmatrix}
|
||||||
|
= {\prod_{1 \leq {i,j} \leq n} {(x_i - x_j)}}
|
||||||
|
$$
|
||||||
|
|
||||||
|
—<cite>[Three famous mathematical formulas](https://developer.mozilla.org/en-US/docs/Learn/MathML/First_steps/Three_famous_mathematical_formulas) (Mozilla Docs)</cite>
|
||||||
|
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
## Foregoing some slop
|
||||||
|
|
||||||
|
- Goodbye, [ESLint](https://eslint.org/)! There have been so many occasions where I've had to deal with blogging templates with in-built pre-commit hooks which enforce contrived and arbitrary linting rules that, frankly, I couldn't be bothered with. Obviously, linting is awesome for ensuring consistency and best practice, but that's with shared and large codebases. You're dealing with, at most, your mediocre MDX blog posts and some interior fetching. It's just not worth the hypertension.
|
||||||
|
|
||||||
|
- You really don't need analytics via [Umami](https://umami.is) or [Plausible](https://plausible.io). Let's be realistic: for many personal blogs, unless you're an anime profile picture Twitter microcelebrity and/or the daughter of Taylor Swift you don't need to know how many of your avid fans click Big Button A versus how many click Big Button B.
|
||||||
|
- You really don't need a comments section via [Giscus](https://giscus.app). This opens up a can of worms involving the ability to spam comments and the necessity to moderate them. If you want organic discussion about your blog posts to happen, then share on social media and let people discuss there.
|
||||||
|
- Speaking of sharing on social media, let's get rid of the share buttons. Please inform me of a single time you have used a share button on a blog post.
|
||||||
|
- You really don't need a <abbr title="Content Management System">CMS</abbr> unless you have thousands of posts and/or are willing to navigate through a clunky management interface. Markdown and folders is really all you need, which you can organize to your preference via folder or file naming conventions.
|
||||||
|
- If you have literally anything involving an `.env` file in a blogging site, please think about what you are doing very carefully.
|
||||||
|
- Please do not override the browser's <kbd>Ctrl</kbd> + <kbd>K</kbd> functionality to open up a command palette. There should not be a single reason why a user would use a small context menu to browse your blog over the `/blog` route. Most of the time, command palettes on sites do nothing more than regurgitate shortcuts that are already on the same page you're hiding with the palette's modal.
|
||||||
|
|
||||||
|
## Something important
|
||||||
|
|
||||||
|
Before we wrap up, I want to emphasize that everything that I've shared here is based on my own personal opinions and experiences. While I believe these practices and choices lead to a better blogging experience, you're absolutely free to disagree.
|
||||||
|
|
||||||
|
The web development community, especially in spaces like Twitter and various online forums, is constantly engaged in heated debates about what constitutes "best practices." You'll find a wide spectrum of viewpoints:
|
||||||
|
|
||||||
|
1. Fundamentalists who adhere strictly to established patterns and completely disregard change,
|
||||||
|
2. Accelerationists who gobble up whatever Vercel cooks as if it's the second coming of Christ,
|
||||||
|
3. and everyone in between this spectrum who just wants to ship.
|
||||||
|
|
||||||
|
I'm just another guy who loves to blog, and I wanted to share what particular technology stack worked the best for me in this particular use case. A stack for one project can be completely unusable for another. If you vehemently hate any of the design choices I've made then simply get rid of them. MIT license! Happy blogging.
|
2
src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
|
@ -1,55 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
width="128"
|
|
||||||
height="128"
|
|
||||||
viewBox="0 0 128 128"
|
|
||||||
version="1.1"
|
|
||||||
xml:space="preserve"
|
|
||||||
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
|
|
||||||
id="svg3"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs3"><linearGradient
|
|
||||||
id="linearGradient26"><stop
|
|
||||||
style="stop-color:#ffffff;stop-opacity:0.30000001;"
|
|
||||||
offset="0"
|
|
||||||
id="stop26" /><stop
|
|
||||||
style="stop-color:#ffffff;stop-opacity:0;"
|
|
||||||
offset="0.9638136"
|
|
||||||
id="stop27" /></linearGradient><radialGradient
|
|
||||||
xlink:href="#linearGradient26"
|
|
||||||
id="radialGradient27"
|
|
||||||
cx="250"
|
|
||||||
cy="250"
|
|
||||||
fx="250"
|
|
||||||
fy="250"
|
|
||||||
r="250"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(0.6,0,0,0.6,100.00001,100.00002)" /><filter
|
|
||||||
style="color-interpolation-filters:sRGB"
|
|
||||||
id="filter1"
|
|
||||||
x="-0.243"
|
|
||||||
y="-0.243"
|
|
||||||
width="1.486"
|
|
||||||
height="1.486"><feGaussianBlur
|
|
||||||
stdDeviation="30.375"
|
|
||||||
id="feGaussianBlur1" /></filter></defs>
|
|
||||||
|
|
||||||
<rect
|
|
||||||
style="clip-rule:evenodd;fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:2"
|
|
||||||
id="bg"
|
|
||||||
width="128"
|
|
||||||
height="128"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
ry="16.384001" /><circle
|
|
||||||
style="display:inline;fill:url(#radialGradient27);fill-opacity:1;stroke-width:0.300003;filter:url(#filter1)"
|
|
||||||
id="backlight"
|
|
||||||
cx="250"
|
|
||||||
cy="250"
|
|
||||||
r="150"
|
|
||||||
transform="matrix(0.256,0,0,0.256,2.2888184e-6,2.2888184e-6)" /><path
|
|
||||||
d="M 64.00001,6.4 68.608,38.656 c 0.79735,4.95473 3.36351,9.8663 6.912,13.41429 3.54847,3.5487 8.86942,6.52423 13.824,7.32171 L 121.6,64 89.344,68.608 C 84.38942,69.40548 79.06847,72.38102 75.52,75.92971 71.97151,79.4777 69.40535,84.38927 68.608,89.344 L 64.00001,121.6 59.392,89.344 C 58.59465,84.38927 56.02847,79.4777 52.48,75.92971 48.93151,72.38102 43.61058,69.40548 38.656,68.608 L 6.4,64 38.656,59.392 C 43.61058,58.59452 48.93151,55.61899 52.48,52.07029 56.02847,48.5223 58.59465,43.61073 59.392,38.656 Z"
|
|
||||||
style="clip-rule:evenodd;display:inline;fill:#ffffff;fill-rule:evenodd;stroke-width:0.0707615;stroke-linejoin:round;stroke-miterlimit:2"
|
|
||||||
id="star" /></svg>
|
|
Before Width: | Height: | Size: 2.3 KiB |
|
@ -1,24 +1,36 @@
|
||||||
---
|
---
|
||||||
import Footer from "@components/Footer.astro";
|
import Footer from '@/components/Footer.astro'
|
||||||
import Head from "@components/Head.astro";
|
import Head from '@/components/Head.astro'
|
||||||
import { SITE } from "@consts";
|
import Header from '@/components/Header.astro'
|
||||||
|
import { SITE } from '@/consts'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string
|
||||||
description: string;
|
description: string
|
||||||
};
|
image?: string
|
||||||
|
}
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
const { title, description, image } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<Head title={`${title} | ${SITE.TITLE}`} description={description} />
|
<Head
|
||||||
|
title={`${title} | ${SITE.TITLE}`}
|
||||||
|
description={description}
|
||||||
|
image={image}
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<div
|
||||||
|
class="box-border flex h-fit min-h-screen flex-col gap-y-6 font-sans antialiased"
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<main class="flex-grow">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(date: Date) {
|
export function formatDate(date: Date) {
|
||||||
return Intl.DateTimeFormat("en-SE", {
|
return Intl.DateTimeFormat('en-US', {
|
||||||
year: "numeric",
|
year: 'numeric',
|
||||||
month: "2-digit",
|
month: 'long',
|
||||||
day: "2-digit",
|
day: 'numeric',
|
||||||
}).format(date);
|
}).format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readingTime(html: string) {
|
export function readingTime(html: string) {
|
||||||
const textOnly = html.replace(/<[^>]+>/g, "");
|
const textOnly = html.replace(/<[^>]+>/g, '')
|
||||||
const wordCount = textOnly.split(/\s+/).length;
|
const wordCount = textOnly.split(/\s+/).length
|
||||||
const readingTimeMinutes = (wordCount / 200 + 1).toFixed();
|
const readingTimeMinutes = (wordCount / 200 + 1).toFixed()
|
||||||
return `${readingTimeMinutes} min read`;
|
return `${readingTimeMinutes} min read`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,37 @@
|
||||||
---
|
---
|
||||||
import Container from "@components/Container.astro";
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import { SITE } from "@consts";
|
import Container from '@/components/Container.astro'
|
||||||
import Layout from "@layouts/Layout.astro";
|
import Link from '@/components/Link.astro'
|
||||||
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
|
import { SITE } from '@/consts'
|
||||||
|
import Layout from '@/layouts/Layout.astro'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404" description={SITE.DESCRIPTION}>
|
<Layout title="404" description={SITE.DESCRIPTION}>
|
||||||
<Container>
|
<Container class="flex grow flex-col gap-y-6">
|
||||||
<div class="text-center">
|
<Breadcrumbs items={[{ label: '???', icon: 'lucide:circle-help' }]} />
|
||||||
<h4 class="text-2xl font-semibold text-black dark:text-white">
|
|
||||||
404: Page not found
|
<section
|
||||||
</h4>
|
class="flex flex-col items-center justify-center gap-y-4 text-center"
|
||||||
|
>
|
||||||
|
<div class="max-w-md">
|
||||||
|
<h1 class="mb-4 text-3xl font-bold">404: Page not found</h1>
|
||||||
|
<p class="prose prose-neutral dark:prose-invert">
|
||||||
|
Oops! The page you're looking for doesn't exist.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant: 'outline' }),
|
||||||
|
'flex gap-x-1.5 group',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span class="transition-transform group-hover:-translate-x-1"
|
||||||
|
>←</span
|
||||||
|
> Go to home page
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
---
|
---
|
||||||
|
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
||||||
|
import PostNavigation from "@/components/PostNavigation.astro";
|
||||||
|
import TableOfContents from "@/components/TableOfContents.astro";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { formatDate, readingTime } from "@/lib/utils";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
import { type CollectionEntry, getCollection, render } from "astro:content";
|
import { type CollectionEntry, getCollection, render } from "astro:content";
|
||||||
import Container from "@components/Container.astro";
|
|
||||||
import FormattedDate from "@components/FormattedDate.astro";
|
|
||||||
import PostNavigation from "@components/PostNavigation.astro";
|
|
||||||
import TableOfContents from "@components/TableOfContents.astro";
|
|
||||||
import Layout from "@layouts/Layout.astro";
|
|
||||||
import { readingTime } from "@lib/utils";
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = (await getCollection("blog"))
|
const posts = (await getCollection("blog"))
|
||||||
|
@ -22,55 +25,121 @@ const posts = (await getCollection("blog"))
|
||||||
.filter((post) => !post.data.draft)
|
.filter((post) => !post.data.draft)
|
||||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||||
|
|
||||||
function getNextPost() {
|
function getPostIndex(id: string): number {
|
||||||
let postIndex;
|
return posts.findIndex((post) => post.id === id);
|
||||||
for (const post of posts) {
|
|
||||||
if (post.id === Astro.params.id) {
|
|
||||||
postIndex = posts.indexOf(post);
|
|
||||||
return posts[postIndex + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrevPost() {
|
function getPrevPost(id: string): Props | null {
|
||||||
let postIndex;
|
const postIndex = getPostIndex(id);
|
||||||
for (const post of posts) {
|
return postIndex !== -1 && postIndex < posts.length - 1
|
||||||
if (post.id === Astro.params.id) {
|
? posts[postIndex + 1]
|
||||||
postIndex = posts.indexOf(post);
|
: null;
|
||||||
return posts[postIndex - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPost = getNextPost();
|
function getNextPost(id: string): Props | null {
|
||||||
const prevPost = getPrevPost();
|
const postIndex = getPostIndex(id);
|
||||||
|
return postIndex > 0 ? posts[postIndex - 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPostId = Astro.params.id;
|
||||||
|
const nextPost = getNextPost(currentPostId);
|
||||||
|
const prevPost = getPrevPost(currentPostId);
|
||||||
|
|
||||||
const post = Astro.props;
|
const post = Astro.props;
|
||||||
const { Content, headings } = await render(post);
|
const { Content, headings } = await render(post);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={post.data.title} description={post.data.description}>
|
<Layout
|
||||||
<Container>
|
title={post.data.title}
|
||||||
<div class="my-10 space-y-1">
|
description={post.data.description}
|
||||||
<div class="flex items-center gap-1.5">
|
image={post.data.image?.src ?? "/static/1200x630.png"}
|
||||||
<div class="font-base text-sm">
|
>
|
||||||
<FormattedDate date={post.data.date} />
|
<section
|
||||||
</div>
|
class="grid grid-cols-[minmax(0px,1fr)_min(768px,100%)_minmax(0px,1fr)] gap-y-6 *:px-4"
|
||||||
•
|
>
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ href: "/", label: "", icon: "lucide:archive" },
|
||||||
|
{ label: post.data.title, icon: "lucide:file-text" },
|
||||||
|
]}
|
||||||
|
class="col-start-2"
|
||||||
|
/>
|
||||||
{
|
{
|
||||||
post.body && (
|
post.data.image && (
|
||||||
<div class="font-base text-sm">{readingTime(post.body)}</div>
|
<Image
|
||||||
|
src={post.data.image}
|
||||||
|
alt={post.data.title}
|
||||||
|
width={1200}
|
||||||
|
height={630}
|
||||||
|
class="col-span-full mx-auto w-full max-w-[1000px] object-cover"
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
<section class="col-start-2 flex flex-col gap-y-6 text-center">
|
||||||
<h1 class="text-3xl font-semibold text-black dark:text-white">
|
<div class="flex flex-col gap-y-4">
|
||||||
|
<h1 class="text-4xl font-bold leading-tight sm:text-5xl">
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-center gap-2 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{formatDate(post.data.date)}</span>
|
||||||
|
<Separator orientation="vertical" className="h-4" />
|
||||||
|
<span>{readingTime(post.body!)}</span>
|
||||||
</div>
|
</div>
|
||||||
{headings.length > 0 && <TableOfContents headings={headings} />}
|
</div>
|
||||||
<article>
|
</div>
|
||||||
<Content />
|
|
||||||
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{headings.length > 0 && <TableOfContents headings={headings} />}
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="prose prose-neutral col-start-2 max-w-none dark:prose-invert"
|
||||||
|
>
|
||||||
|
<Content />
|
||||||
</article>
|
</article>
|
||||||
</Container>
|
|
||||||
|
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="group fixed bottom-8 right-8 z-50 hidden"
|
||||||
|
id="scroll-to-top"
|
||||||
|
title="Scroll to top"
|
||||||
|
aria-label="Scroll to top"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="lucide:arrow-up"
|
||||||
|
class="mx-auto size-4 transition-all group-hover:-translate-y-0.5"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
const scrollToTopButton = document.getElementById("scroll-to-top");
|
||||||
|
const footer = document.querySelector("footer");
|
||||||
|
|
||||||
|
if (scrollToTopButton && footer) {
|
||||||
|
scrollToTopButton.addEventListener("click", () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("scroll", () => {
|
||||||
|
const footerRect = footer.getBoundingClientRect();
|
||||||
|
const isFooterVisible = footerRect.top <= window.innerHeight;
|
||||||
|
|
||||||
|
scrollToTopButton.classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
window.scrollY <= 300 || isFooterVisible,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
72
src/pages/[...page].astro
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
---
|
||||||
|
import BlogCard from "@/components/BlogCard.astro";
|
||||||
|
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
||||||
|
import Container from "@/components/Container.astro";
|
||||||
|
import PaginationComponent from "@/components/ui/pagination";
|
||||||
|
import { SITE } from "@/consts";
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import type { PaginateFunction } from "astro";
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
|
||||||
|
export async function getStaticPaths({
|
||||||
|
paginate,
|
||||||
|
}: {
|
||||||
|
paginate: PaginateFunction;
|
||||||
|
}) {
|
||||||
|
const allPosts = await getCollection("blog", ({ data }) => !data.draft);
|
||||||
|
return paginate(
|
||||||
|
allPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()),
|
||||||
|
{ pageSize: SITE.POSTS_PER_PAGE },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
|
||||||
|
const postsByYear = page.data.reduce(
|
||||||
|
(acc: Record<string, CollectionEntry<"blog">[]>, post) => {
|
||||||
|
const year = post.data.date.getFullYear().toString();
|
||||||
|
(acc[year] ??= []).push(post);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const years = Object.keys(postsByYear).sort(
|
||||||
|
(a, b) => parseInt(b) - parseInt(a),
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="home" description={SITE.DESCRIPTION}>
|
||||||
|
<Container class="flex grow flex-col gap-y-6">
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ label: "", href: "/", icon: "lucide:archive" },
|
||||||
|
{ label: `Page ${page.currentPage}`, icon: "lucide:folder-open" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
|
||||||
|
{
|
||||||
|
years.map((year) => (
|
||||||
|
<section class="flex flex-col gap-y-4">
|
||||||
|
<div class="font-semibold">{year}</div>
|
||||||
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
|
{postsByYear[year].map((post) => (
|
||||||
|
<li>
|
||||||
|
<BlogCard entry={post} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PaginationComponent
|
||||||
|
currentPage={page.currentPage}
|
||||||
|
totalPages={page.lastPage}
|
||||||
|
baseUrl="/"
|
||||||
|
client:load
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Layout>
|
|
@ -1,58 +0,0 @@
|
||||||
---
|
|
||||||
import { type CollectionEntry, getCollection } from "astro:content";
|
|
||||||
import ArrowCard from "@components/ArrowCard.astro";
|
|
||||||
import Container from "@components/Container.astro";
|
|
||||||
import { SITE } from "@consts";
|
|
||||||
import { HOME } from "@consts";
|
|
||||||
import Layout from "@layouts/Layout.astro";
|
|
||||||
|
|
||||||
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) => Number.parseInt(b) - Number.parseInt(a),
|
|
||||||
);
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={HOME.TITLE} description={SITE.DESCRIPTION}>
|
|
||||||
<Container>
|
|
||||||
<aside>
|
|
||||||
<div class="space-y-10">
|
|
||||||
<div class="space-y-4">
|
|
||||||
{
|
|
||||||
years.map((year) => (
|
|
||||||
<section class="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>
|
|
62
src/pages/robots.txt.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
|
const getRobotsTxt = (sitemapURL: URL) => `
|
||||||
|
User-agent: AI2Bot
|
||||||
|
User-agent: Ai2Bot-Dolma
|
||||||
|
User-agent: Amazonbot
|
||||||
|
User-agent: anthropic-ai
|
||||||
|
User-agent: Applebot
|
||||||
|
User-agent: Applebot-Extended
|
||||||
|
User-agent: Bytespider
|
||||||
|
User-agent: CCBot
|
||||||
|
User-agent: ChatGPT-User
|
||||||
|
User-agent: Claude-Web
|
||||||
|
User-agent: ClaudeBot
|
||||||
|
User-agent: cohere-ai
|
||||||
|
User-agent: cohere-training-data-crawler
|
||||||
|
User-agent: Crawlspace
|
||||||
|
User-agent: Diffbot
|
||||||
|
User-agent: DuckAssistBot
|
||||||
|
User-agent: FacebookBot
|
||||||
|
User-agent: FriendlyCrawler
|
||||||
|
User-agent: Google-Extended
|
||||||
|
User-agent: GoogleOther
|
||||||
|
User-agent: GoogleOther-Image
|
||||||
|
User-agent: GoogleOther-Video
|
||||||
|
User-agent: GPTBot
|
||||||
|
User-agent: iaskspider/2.0
|
||||||
|
User-agent: ICC-Crawler
|
||||||
|
User-agent: ImagesiftBot
|
||||||
|
User-agent: img2dataset
|
||||||
|
User-agent: ISSCyberRiskCrawler
|
||||||
|
User-agent: Kangaroo Bot
|
||||||
|
User-agent: Meta-ExternalAgent
|
||||||
|
User-agent: Meta-ExternalFetcher
|
||||||
|
User-agent: OAI-SearchBot
|
||||||
|
User-agent: omgili
|
||||||
|
User-agent: omgilibot
|
||||||
|
User-agent: PanguBot
|
||||||
|
User-agent: PerplexityBot
|
||||||
|
User-agent: PetalBot
|
||||||
|
User-agent: Scrapy
|
||||||
|
User-agent: SemrushBot-OCOB
|
||||||
|
User-agent: SemrushBot-SWA
|
||||||
|
User-agent: Sidetrade indexer bot
|
||||||
|
User-agent: Timpibot
|
||||||
|
User-agent: VelenPublicWebCrawler
|
||||||
|
User-agent: Webzio-Extended
|
||||||
|
User-agent: YouBot
|
||||||
|
Disallow: /
|
||||||
|
DisallowAITraining: /
|
||||||
|
|
||||||
|
# Block any non-specified AI crawlers
|
||||||
|
User-Agent: *
|
||||||
|
DisallowAITraining: /within
|
||||||
|
|
||||||
|
Sitemap: ${sitemapURL.href}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GET: APIRoute = ({ site }) => {
|
||||||
|
const sitemapURL = new URL('sitemap-index.xml', site)
|
||||||
|
return new Response(getRobotsTxt(sitemapURL))
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
import { getCollection } from "astro:content";
|
|
||||||
import rss from "@astrojs/rss";
|
|
||||||
import { SITE } from "@consts";
|
|
||||||
|
|
||||||
export async function GET(context) {
|
|
||||||
const blog = (await getCollection("blog")).filter((post) => !post.data.draft);
|
|
||||||
|
|
||||||
const items = [...blog].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.id}/`,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
32
src/pages/rss.xml.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { SITE } from '@/consts'
|
||||||
|
import rss from '@astrojs/rss'
|
||||||
|
import type { APIContext } from 'astro'
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
export async function GET(context: APIContext) {
|
||||||
|
try {
|
||||||
|
const blog = (await getCollection('blog')).filter(
|
||||||
|
(post) => !post.data.draft,
|
||||||
|
)
|
||||||
|
|
||||||
|
const items = [...blog].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 ?? SITE.SITEURL,
|
||||||
|
items: items.map((item) => ({
|
||||||
|
title: item.data.title,
|
||||||
|
description: item.data.description,
|
||||||
|
pubDate: item.data.date,
|
||||||
|
link: `/${item.id}/`,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating RSS feed:', error)
|
||||||
|
return new Response('Error generating RSS feed', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,107 +2,190 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
html {
|
@font-face {
|
||||||
overflow-y: auto;
|
font-family: 'Geist';
|
||||||
color-scheme: light;
|
src: url('/fonts/GeistVF.woff2') format('woff2');
|
||||||
scroll-padding-top: 100px;
|
font-weight: 100 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark {
|
@font-face {
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
src: url('/fonts/GeistMonoVF.woff2') format('woff2');
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
--primary: 0 0% 9%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 0 0% 80.1%;
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
--muted: 0 0% 80.1%;
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
--accent: 0 0% 80.1%;
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
--additive: 112 50% 36%;
|
||||||
|
--additive-foreground: 0 0% 9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 0 0% 9%;
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--additive: 112 50% 36%;
|
||||||
|
--additive-foreground: 0 0% 9%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
--ring: 0 0% 83.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
color-scheme: light;
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
|
||||||
|
&.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
::-webkit-scrollbar-corner {
|
||||||
body {
|
@apply bg-transparent;
|
||||||
@apply size-full;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.disable-transitions,
|
||||||
@apply font-sans antialiased;
|
.disable-transitions * {
|
||||||
@apply flex flex-col;
|
@apply !transition-none;
|
||||||
@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-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
@apply flex-1 py-10;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
@apply py-6 text-sm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
article {
|
article {
|
||||||
@apply prose prose-neutral max-w-full dark:prose-invert prose-img:mx-auto prose-img:my-auto;
|
@apply prose-headings:scroll-mt-20 prose-headings:break-words first:prose-headings:mt-0 prose-p:break-words prose-a:!break-words prose-a:!decoration-muted-foreground prose-a:underline-offset-[3px] prose-a:transition-colors hover:prose-a:!decoration-foreground prose-pre:!px-0 prose-img:mx-auto;
|
||||||
@apply prose-headings:font-semibold;
|
|
||||||
@apply prose-headings:text-black prose-headings:dark:text-white;
|
.katex-display {
|
||||||
|
@apply overflow-x-auto overflow-y-hidden py-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
/* Removes background from <mark> elements */
|
||||||
article a {
|
mark {
|
||||||
@apply font-sans text-current underline underline-offset-[3px];
|
@apply bg-transparent;
|
||||||
@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;
|
/* Blanket syntax highlighting */
|
||||||
@apply decoration-black/50 dark:decoration-white/50;
|
code[data-theme*=' '] {
|
||||||
|
span {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & span {
|
||||||
|
color: var(--shiki-dark);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
html #back-to-top {
|
:not(pre) > code {
|
||||||
@apply pointer-events-none opacity-0;
|
@apply relative rounded bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.scrolled #back-to-top {
|
/* Code blocks */
|
||||||
@apply pointer-events-auto opacity-100;
|
figure[data-rehype-pretty-code-figure] {
|
||||||
|
@apply relative;
|
||||||
|
|
||||||
|
/* Code block titles */
|
||||||
|
[data-rehype-pretty-code-title] {
|
||||||
|
@apply break-words rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium text-foreground;
|
||||||
|
|
||||||
|
/* Remove top margin from code block if a title is present */
|
||||||
|
& + pre {
|
||||||
|
@apply mt-0 rounded-t-none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shadcn-like scrollbar */
|
||||||
|
pre::-webkit-scrollbar {
|
||||||
|
@apply h-2.5 w-2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre::-webkit-scrollbar-track {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre::-webkit-scrollbar-thumb {
|
||||||
|
@apply rounded-full bg-border bg-clip-padding p-px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code block styles */
|
||||||
pre {
|
pre {
|
||||||
@apply border border-black/15 py-5 dark:border-white/20;
|
@apply static max-h-[600px] overflow-auto rounded-xl border bg-secondary/20 py-4 text-sm leading-loose;
|
||||||
|
|
||||||
|
/* Code block content */
|
||||||
|
> code {
|
||||||
|
@apply whitespace-pre-wrap;
|
||||||
|
counter-reset: line;
|
||||||
|
|
||||||
|
/* For code blocks with line numbers */
|
||||||
|
&[data-line-numbers] {
|
||||||
|
> [data-line]::before {
|
||||||
|
counter-increment: line;
|
||||||
|
content: counter(line);
|
||||||
|
@apply mr-4 inline-block w-4 text-right text-muted-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
/* For each line in the code block */
|
||||||
--astro-code-foreground: #09090b;
|
> [data-line] {
|
||||||
--astro-code-background: #fafafa;
|
@apply px-4;
|
||||||
--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 {
|
/* Highlighted lines */
|
||||||
--astro-code-foreground: #fafafa;
|
[data-highlighted-line] {
|
||||||
--astro-code-background: #09090b;
|
@apply bg-foreground/10;
|
||||||
--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 {
|
/* Highlighted characters */
|
||||||
@apply absolute right-3 top-3 grid size-9 place-content-center rounded border border-black/15 bg-neutral-100 text-center duration-300 ease-in-out dark:border-white/20 dark:bg-neutral-900;
|
[data-highlighted-chars] > span {
|
||||||
|
@apply bg-muted-foreground/40 py-[7px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Diff lines */
|
||||||
|
.diff {
|
||||||
|
&.add {
|
||||||
|
@apply bg-additive/15;
|
||||||
|
}
|
||||||
|
&.remove {
|
||||||
|
@apply bg-destructive/15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy button */
|
||||||
|
> button:has(> span) {
|
||||||
|
@apply right-0.5 top-[3px] m-0 size-8 rounded-md bg-background p-1 backdrop-blur-none transition-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.copy-code:hover {
|
|
||||||
@apply bg-[#E9E9E9] transition-colors dark:bg-[#232323];
|
|
||||||
}
|
}
|
||||||
.copy-code:active {
|
|
||||||
@apply scale-90 transition-transform;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
export type Site = {
|
|
||||||
TITLE: string;
|
|
||||||
DESCRIPTION: string;
|
|
||||||
};
|
|
||||||
export type Metadata = {
|
|
||||||
TITLE: string;
|
|
||||||
};
|
|
|
@ -1,14 +0,0 @@
|
||||||
import defaultTheme from "tailwindcss/defaultTheme";
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
darkMode: "class",
|
|
||||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: ["Nunito", ...defaultTheme.fontFamily.sans],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require("@tailwindcss/typography")],
|
|
||||||
};
|
|
48
tailwind.config.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: ['selector'],
|
||||||
|
content: ['./src/**/*.{astro,md,mdx,ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Geist', ...defaultTheme.fontFamily.sans],
|
||||||
|
mono: ['Geist Mono', ...defaultTheme.fontFamily.mono],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
additive: {
|
||||||
|
DEFAULT: 'hsl(var(--additive))',
|
||||||
|
foreground: 'hsl(var(--additive-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/typography'), require('tailwindcss-animate')],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"include": [".astro/types.d.ts", "**/*"],
|
|
||||||
"exclude": ["dist"],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|