diff --git a/app/categories/[categorySlug]/page.tsx b/app/categories/[categorySlug]/page.tsx new file mode 100644 index 0000000..58e8eec --- /dev/null +++ b/app/categories/[categorySlug]/page.tsx @@ -0,0 +1,48 @@ +import { metadata } from '@/app/metadata' +import { getCategoryBySlug, getPostsByCategory } from '@/lib/categories' +import type { Metadata } from 'next' +import Link from 'next/link' + +type CategoryPageProps = { + params: Promise<{ + categorySlug: string + }> +} + +export async function generateMetadata({ params }: CategoryPageProps): Promise { + const { categorySlug } = await params + const category = getCategoryBySlug(categorySlug) + + return { + title: `${category} | ${metadata.title!.toString()}`, + description: 'List of posts in this category', + } +} + +export default async function CategoryPage({ params }: CategoryPageProps) { + const { categorySlug } = await params + const posts = getPostsByCategory(categorySlug) + const category = getCategoryBySlug(categorySlug) + + if (!category) { + return ( +
+

Category not found

+ Back to categories +
+ ) + } + + return ( +
+

Category: {category}

+ +
+ ) +} diff --git a/app/categories/[category]/page.tsx b/app/categories/[category]/page.tsx deleted file mode 100644 index 7189e98..0000000 --- a/app/categories/[category]/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -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/categories/page.tsx b/app/categories/page.tsx index 24a42d5..73ddf6f 100644 --- a/app/categories/page.tsx +++ b/app/categories/page.tsx @@ -1,34 +1,43 @@ -import { getAllPostsMeta } from "@/lib/posts" -import type { Metadata } from "next" -import Link from "next/link" +import { getAllCategories } from '@/lib/categories' +import { getAllPostsMeta } from '@/lib/posts' +import { slugify } from '@/utils/slug' +import type { Metadata } from 'next' +import Link from 'next/link' +import { metadata } from '@/app/metadata' export async function generateMetadata(): Promise { - return { title: "Categories", description: "List of categories" } + return { title: `Categories | ${metadata.title!.toString()}`, description: 'List of categories' } } export default function CategoriesPage() { + const categories = getAllCategories() const posts = getAllPostsMeta() + return ( -
-

Categories

-
    - {Array.from(new Set(posts.flatMap((post) => post.categories))).map( - (category) => ( +
    +
    +

    Categories

    +
      + {Object.keys(categories).map((category) => (
    • -

      {category}

      + + {category} ({categories[category]}) + +
        {posts - .filter((post) => post.categories.includes(category)) + .filter((post) => post.category === category) .map((post) => (
      • - {post.title} + {post.title}
      • ))}
    • - ), - )} -
    -
    + ))} +
+
+
+
) } diff --git a/app/feed.xml/route.ts b/app/feed.xml/route.ts index 1cf9099..52dab6f 100644 --- a/app/feed.xml/route.ts +++ b/app/feed.xml/route.ts @@ -1,4 +1,4 @@ -import meta from "@/const/meta" +import { metadata } from "@/app/metadata" import { getAllPostsMeta } from "@/lib/posts" const SITE_URL = process.env.SITE_URL || "http://localhost:3000" @@ -12,7 +12,7 @@ function escapeXml(str: string) { .replace(/'/g, "'") } -export async function GET() { +export async function GET(req: Request) { const posts = getAllPostsMeta() const updated = @@ -39,18 +39,24 @@ export async function GET() { const feed = ` - ${escapeXml(meta.title)} ${SITE_URL}/ + ${escapeXml(metadata.title!.toString())} ${updated} - ${escapeXml("d")} + ${escapeXml(metadata.creator!.toString())} + © ${new Date().getFullYear()} Frank Borisjuk ${entries} ` + const accept = req.headers.get("accept") || "" + const contentType = accept.includes("application/atom+xml") + ? "application/atom+xml; charset=utf-8" + : "application/xml; charset=utf-8" + return new Response(feed, { - headers: { "Content-Type": "application/atom+xml; charset=utf-8" }, + headers: { "Content-Type": contentType }, }) } \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 2025da7..fd6949c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,14 +4,16 @@ @theme { --color-bg: #040404; --color-fg: #e0e0e0; - --color-fg-dim: #909090; + --color-fg-dim: #b0b0b0; --color-accent: #ef1f1f; --color-border: #374151; --color-warning: #facc15; --color-tip: #22c55e; --color-info: #3b82f6; - --color-note: #8b5cf6; + --color-quote: #f97316; + + --font-sans: 'Space Grotesk', sans-serif; } .hljs { @@ -22,19 +24,27 @@ pre code.hljs { @apply p-0; } +* { + @apply scroll-smooth; +} + body { @apply bg-bg text-fg; } .prose { - @apply space-y-4; + @apply space-y-4 text-fg-dim; ul { - @apply list-disc list-inside; + @apply list-disc; } ol { - @apply list-decimal list-inside; + @apply list-decimal; + } + + ul, ol { + @apply list-outside; } li { @@ -75,6 +85,10 @@ tbody tr:nth-child(even) { @apply bg-fg/5; } +tbody tr:hover { + @apply bg-fg/10; +} + /* ── Definition lists ─────────────────────────────── */ dl { @@ -125,7 +139,7 @@ dd { .prose h4, .prose h5, .prose h6 { - @apply font-bold mb-2; + @apply font-bold mb-2 text-fg; &::before { @apply content-['#'] text-accent mr-1; @@ -183,7 +197,7 @@ hr { } .note { - @apply border-l-4 pl-4 py-1 text-fg-dim bg-accent/20; + @apply border-l-4 pl-4 py-2 text-fg-dim bg-border/20; &.warning { @apply border-warning bg-warning/20 text-warning; @@ -196,6 +210,10 @@ hr { &.info { @apply border-info bg-info/20 text-info; } + + &.quote { + @apply border-quote bg-quote/20 text-quote; + } } .no-js .js-only { diff --git a/app/layout.tsx b/app/layout.tsx index 009bad6..6c4d75b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,36 +1,22 @@ -import { Geist, Geist_Mono } from 'next/font/google' -import './globals.css' -import { Metadata } from 'next' -import meta from '@/const/meta' +import { Space_Grotesk } from 'next/font/google' import Script from 'next/script' import { Sidebar } from '@/components/layout/Sidebar' +import { metadata } from './metadata' +import './globals.css' -const geistSans = Geist({ - variable: '--font-geist-sans', +const spaceGrotesk = Space_Grotesk({ subsets: ['latin'], + variable: '--font-sans', + display: 'swap', + weight: ['400', '500', '600', '700'], }) - -const geistMono = Geist_Mono({ - variable: '--font-geist-mono', - subsets: ['latin'], -}) - -export const metadata: Metadata = { - title: meta.title, - description: meta.description, - creator: meta.creator, -} - export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - +