diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..4baacea --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,18 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "insertFinalNewline": true, + "ignorePatterns": [ + "node_modules/**", + ".cache/**", + "_site/**" + ] +} \ No newline at end of file diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..e36c434 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,24 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "env": { + "browser": true, + "node": true, + "es2024": true + }, + "plugins": ["typescript", "react", "unicorn", "import", "oxc"], + "categories": { + "correctness": "error", + "suspicious": "warn", + "perf": "warn" + }, + "rules": { + "eqeqeq": "error", + "no-unused-vars": "off", + "typescript/no-unused-vars": "warn", + "typescript/no-explicit-any": "warn", + "import/no-cycle": "error", + "prefer-const": ["error", { "ignoreReadBeforeAssign": true }], + "react/react-in-jsx-scope": "allow" + }, + "ignorePatterns": ["node_modules/**", ".cache/**", "_site/**"] +} diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx deleted file mode 100644 index a91cd4d..0000000 --- a/app/[slug]/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { getAllPostSlugs, getPostBySlug } from "@/lib/posts" -import type { Metadata } from "next" -import readingTime from "reading-time" - -export async function generateStaticParams() { - return getAllPostSlugs().map((slug) => ({ slug })) -} - -export const dynamicParams = false - -export async function generateMetadata({ - params, -}: { - params: Promise<{ slug: string }> -}): Promise { - const { slug } = await params - const { meta } = await getPostBySlug(slug) - return { title: meta.title, description: meta.description } -} - -export default async function Post({ - params, -}: { - params: Promise<{ slug: string }> -}) { - const { slug } = await params - const { meta, contentHtml } = await getPostBySlug(slug) - - return ( -
-

{meta.title}

- - {readingTime(contentHtml).text} -
-
- ) -} diff --git a/app/categories/[category]/page.tsx b/app/categories/[category]/page.tsx new file mode 100644 index 0000000..7189e98 --- /dev/null +++ b/app/categories/[category]/page.tsx @@ -0,0 +1,37 @@ +import { getAllPostsMeta } from "@/lib/posts" +import type { Metadata } from "next" +import Link from "next/link" + +type CategoryPageProps = { + params: Promise<{ + category: string + }> +} + +export async function generateMetadata({ + params, +}: CategoryPageProps): Promise { + const { category } = await params + return { title: `Category: ${category}`, description: "List of posts in this category" } +} + +export default async function CategoryPage({ + params, +}: CategoryPageProps) { + const { category } = await params + const posts = getAllPostsMeta() + return ( +
+

Category: {category}

+
    + {posts + .filter((post) => post.categories.includes(category)) + .map((post) => ( +
  • + {post.title} +
  • + ))} +
+
+ ) +} diff --git a/app/feed.xml/route.ts b/app/feed.xml/route.ts index 4c118cd..1cf9099 100644 --- a/app/feed.xml/route.ts +++ b/app/feed.xml/route.ts @@ -1,29 +1,56 @@ import meta from "@/const/meta" import { getAllPostsMeta } from "@/lib/posts" +const SITE_URL = process.env.SITE_URL || "http://localhost:3000" + +function escapeXml(str: string) { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") +} + export async function GET() { const posts = getAllPostsMeta() - const items = posts - .map( - (p) => ` - - ${p.title} - https://yourdomain.com/blog/${p.slug} - ${p.description} - ${new Date(p.date).toUTCString()} - `, - ) + + const updated = + posts.length > 0 + ? new Date(posts[0].date).toISOString() + : new Date().toISOString() + + const entries = posts + .map((p) => { + const url = `${SITE_URL}/posts/${p.slug}` + const date = new Date(p.date).toISOString() + return ` + + ${escapeXml(p.title)} + + ${url} + ${date} + ${date} + + ${escapeXml(p.description)} + ` + }) .join("") const feed = ` - - ${meta.title} - https://yourdomain.com - Latest posts - ${items} -` + + ${escapeXml(meta.title)} + ${SITE_URL}/ + ${updated} + + ${escapeXml("d")} + + + + ${entries} +` return new Response(feed, { - headers: { "Content-Type": "application/xml" }, + headers: { "Content-Type": "application/atom+xml; charset=utf-8" }, }) -} +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index d1553ff..2025da7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,31 +1,34 @@ -@import "tailwindcss"; +@import 'tailwindcss'; +@import '../node_modules/highlight.js/styles/monokai.min.css'; -:root { - --background: #ffffff; - --foreground: #171717; +@theme { + --color-bg: #040404; + --color-fg: #e0e0e0; + --color-fg-dim: #909090; + --color-accent: #ef1f1f; + --color-border: #374151; + + --color-warning: #facc15; + --color-tip: #22c55e; + --color-info: #3b82f6; + --color-note: #8b5cf6; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); +.hljs { + @apply bg-transparent; } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +pre code.hljs { + @apply p-0; } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + @apply bg-bg text-fg; } -.custom-prose { +.prose { + @apply space-y-4; + ul { @apply list-disc list-inside; } @@ -38,3 +41,167 @@ body { @apply mb-1; } } + +.categories::before { + @apply content-['$'] text-accent; +} + +.handle::before { + @apply content-['@'] text-current; +} + +/* ── Mark ────────────────────────────── */ + +mark { + @apply bg-accent/20 px-1 py-0.5 rounded text-accent; +} + +/* ── Tables ────────────────────────────────────────── */ + +table { + @apply border-collapse w-full; +} + +th, +td { + @apply border border-border px-4 py-2; +} + +th { + @apply bg-fg/10; +} + +tbody tr:nth-child(even) { + @apply bg-fg/5; +} + +/* ── Definition lists ─────────────────────────────── */ + +dl { + @apply my-4; +} + +dt { + @apply font-semibold text-fg mt-3; +} + +dt:first-child { + @apply mt-0; +} + +dd { + @apply ml-6 pl-3 border-l-2 border-fg/20 text-fg-dim; +} + +/* ── Prose (markdown) content ─────────────────────── */ + +.prose h1 { + @apply text-3xl md:text-4xl; +} + +.prose h2 { + @apply text-2xl md:text-3xl; +} + +.prose h3 { + @apply text-xl md:text-2xl; +} + +.prose h4 { + @apply text-lg md:text-xl; +} + +.prose h5 { + @apply text-base md:text-lg; +} + +.prose h6 { + @apply text-sm md:text-base; +} + +.prose h1, +.prose h2, +.prose h3, +.prose h4, +.prose h5, +.prose h6 { + @apply font-bold mb-2; + + &::before { + @apply content-['#'] text-accent mr-1; + } +} + +.prose code:not(pre code) { + @apply text-sm bg-fg/10 px-1 py-0.5 rounded; +} + +.prose a { + @apply text-accent; +} + +.prose ul:not(.task-list-container) { + @apply list-disc pl-6; +} + +hr { + @apply text-border; +} + +/* Only ol that do not include checkboxes */ +.prose ol { + @apply list-decimal pl-6; +} + +/* ── Task lists ────────────────────────────────────── */ + +.task-list-item { + @apply list-none ml-0 pl-0; +} + +.task-list-item input[type='checkbox'] { + @apply mr-2 accent-accent w-4 h-4 align-middle cursor-pointer; +} + +.task-list-item input[type='checkbox']:checked + label, +.task-list-item input[type='checkbox'][checked] + label { + @apply text-fg-dim line-through; +} + +/* ── Footnotes ─────────────────────────────────────── */ + +.footnotes { + @apply text-sm italic text-fg-dim pt-4 mt-4 border-t border-border; +} + +.footnotes ol { + @apply space-y-2; +} + +.sr-only { + @apply sr-only; +} + +.note { + @apply border-l-4 pl-4 py-1 text-fg-dim bg-accent/20; + + &.warning { + @apply border-warning bg-warning/20 text-warning; + } + + &.tip { + @apply border-tip bg-tip/20 text-tip; + } + + &.info { + @apply border-info bg-info/20 text-info; + } +} + +.no-js .js-only { + @apply hidden; +} + +#mermaid-diagram { + @apply mx-auto; +} diff --git a/app/layout.tsx b/app/layout.tsx index 1ecc759..009bad6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,16 +1,18 @@ -import { Geist, Geist_Mono } from "next/font/google" -import "./globals.css" -import { Metadata } from "next" -import meta from "@/const/meta" +import { Geist, Geist_Mono } from 'next/font/google' +import './globals.css' +import { Metadata } from 'next' +import meta from '@/const/meta' +import Script from 'next/script' +import { Sidebar } from '@/components/layout/Sidebar' const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: '--font-geist-sans', + subsets: ['latin'], }) const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: '--font-geist-mono', + subsets: ['latin'], }) export const metadata: Metadata = { @@ -27,9 +29,17 @@ export default function RootLayout({ return ( - {children} +