create-unmint 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +57 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +499 -0
  4. package/package.json +48 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +278 -0
  7. package/template/__tests__/components/callout.test.tsx +46 -0
  8. package/template/__tests__/components/card.test.tsx +59 -0
  9. package/template/__tests__/components/tabs.test.tsx +61 -0
  10. package/template/__tests__/theme-config.test.ts +49 -0
  11. package/template/__tests__/utils.test.ts +25 -0
  12. package/template/app/api/og/route.tsx +90 -0
  13. package/template/app/api/search/route.ts +6 -0
  14. package/template/app/components/docs/docs-pager.tsx +41 -0
  15. package/template/app/components/docs/docs-sidebar.tsx +143 -0
  16. package/template/app/components/docs/docs-toc.tsx +61 -0
  17. package/template/app/components/docs/mdx/accordion.tsx +54 -0
  18. package/template/app/components/docs/mdx/callout.tsx +102 -0
  19. package/template/app/components/docs/mdx/card.tsx +110 -0
  20. package/template/app/components/docs/mdx/code-block.tsx +42 -0
  21. package/template/app/components/docs/mdx/frame.tsx +14 -0
  22. package/template/app/components/docs/mdx/index.tsx +167 -0
  23. package/template/app/components/docs/mdx/pre.tsx +82 -0
  24. package/template/app/components/docs/mdx/steps.tsx +59 -0
  25. package/template/app/components/docs/mdx/tabs.tsx +60 -0
  26. package/template/app/components/docs/mdx/youtube.tsx +18 -0
  27. package/template/app/components/docs/search-dialog.tsx +281 -0
  28. package/template/app/components/docs/theme-toggle.tsx +35 -0
  29. package/template/app/docs/[[...slug]]/page.tsx +139 -0
  30. package/template/app/docs/layout.tsx +98 -0
  31. package/template/app/globals.css +151 -0
  32. package/template/app/layout.tsx +33 -0
  33. package/template/app/page.tsx +5 -0
  34. package/template/app/providers/theme-provider.tsx +8 -0
  35. package/template/content/docs/components.mdx +82 -0
  36. package/template/content/docs/customization.mdx +34 -0
  37. package/template/content/docs/deployment.mdx +28 -0
  38. package/template/content/docs/index.mdx +91 -0
  39. package/template/content/docs/meta.json +13 -0
  40. package/template/content/docs/quickstart.mdx +110 -0
  41. package/template/content/docs/theming.mdx +41 -0
  42. package/template/lib/docs-source.ts +7 -0
  43. package/template/lib/theme-config.ts +89 -0
  44. package/template/lib/utils.ts +6 -0
  45. package/template/next.config.mjs +10 -0
  46. package/template/package-lock.json +10695 -0
  47. package/template/package.json +45 -0
  48. package/template/postcss.config.mjs +7 -0
  49. package/template/public/logo.png +0 -0
  50. package/template/public/logo.svg +9 -0
  51. package/template/public/logo.txt +1 -0
  52. package/template/source.config.ts +22 -0
  53. package/template/tailwind.config.ts +34 -0
  54. package/template/tsconfig.json +33 -0
  55. package/template/vitest.config.ts +16 -0
@@ -0,0 +1,278 @@
1
+ # Unmint
2
+
3
+ A free, open-source Mintlify-style documentation system built with Next.js and Fumadocs.
4
+
5
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
6
+
7
+ ## What is Unmint?
8
+
9
+ Unmint provides a beautiful, customizable documentation experience similar to [Mintlify](https://mintlify.com), but completely free and open source. It's designed to be forked, customized, and self-hosted.
10
+
11
+ ### Features
12
+
13
+ - **Beautiful out-of-the-box** - Professional styling without configuration
14
+ - **MDX Components** - Cards, callouts, tabs, steps, accordions, and more
15
+ - **Built-in Search** - Full-text search powered by Fumadocs
16
+ - **Dynamic OG Images** - Auto-generated social preview images
17
+ - **Dark Mode** - Seamless light/dark theme switching
18
+ - **Easy Theming** - Single config file for all customization
19
+ - **SEO Optimized** - Automatic meta tags and sitemap
20
+
21
+ ### What Unmint is NOT
22
+
23
+ - A hosted SaaS (you deploy it yourself)
24
+ - A replacement for all Mintlify features (no built-in analytics, AI search, etc.)
25
+ - A no-code solution (basic Next.js knowledge helpful)
26
+
27
+ ## Quick Start
28
+
29
+ ### Prerequisites
30
+
31
+ - Node.js 18+
32
+ - npm, yarn, or pnpm
33
+
34
+ ### Installation
35
+
36
+ The fastest way to get started is with our CLI:
37
+
38
+ ```bash
39
+ npx create-unmint@latest my-docs
40
+ ```
41
+
42
+ This will prompt you for project configuration and set up everything automatically.
43
+
44
+ #### CLI Options
45
+
46
+ ```bash
47
+ # Create with all defaults (no prompts)
48
+ npx create-unmint@latest my-docs --yes
49
+
50
+ # Update an existing project to latest
51
+ cd my-docs
52
+ npx create-unmint@latest --update
53
+ ```
54
+
55
+ #### Manual Installation
56
+
57
+ You can also clone and customize manually:
58
+
59
+ ```bash
60
+ # Clone the repository
61
+ git clone https://github.com/gregce/unmint.git my-docs
62
+ cd my-docs
63
+
64
+ # Install dependencies
65
+ npm install
66
+
67
+ # Start development server
68
+ npm run dev
69
+ ```
70
+
71
+ Open [http://localhost:3000](http://localhost:3000) to see your docs.
72
+
73
+ ## Project Structure
74
+
75
+ ```
76
+ my-docs/
77
+ ├── app/
78
+ │ ├── docs/ # Documentation routes
79
+ │ │ ├── layout.tsx # Docs layout (header, sidebar, footer)
80
+ │ │ └── [[...slug]]/ # Dynamic page rendering
81
+ │ ├── api/
82
+ │ │ ├── search/ # Search API endpoint
83
+ │ │ └── og/ # OG image generation
84
+ │ └── components/
85
+ │ └── docs/ # Docs-specific components
86
+ │ └── mdx/ # MDX components (Card, Callout, etc.)
87
+ ├── content/
88
+ │ └── docs/ # Your MDX documentation files
89
+ │ ├── index.mdx # Homepage
90
+ │ ├── meta.json # Navigation structure
91
+ │ └── *.mdx # Your pages
92
+ ├── lib/
93
+ │ ├── theme-config.ts # ⭐ Customize your theme here
94
+ │ ├── docs-source.ts # Fumadocs source config
95
+ │ └── utils.ts # Utility functions
96
+ ├── public/ # Static assets (logo, images)
97
+ ├── source.config.ts # MDX/Fumadocs configuration
98
+ └── package.json
99
+ ```
100
+
101
+ ## Customization
102
+
103
+ ### 1. Update Site Info
104
+
105
+ Edit `lib/theme-config.ts`:
106
+
107
+ ```typescript
108
+ export const siteConfig = {
109
+ name: 'My Docs',
110
+ description: 'Documentation for My Project',
111
+ url: 'https://docs.example.com',
112
+ logo: {
113
+ src: '/logo.png',
114
+ alt: 'My Project',
115
+ width: 32,
116
+ height: 32,
117
+ },
118
+ links: {
119
+ github: 'https://github.com/your-org/your-repo',
120
+ discord: 'https://discord.gg/your-invite',
121
+ support: 'mailto:support@example.com',
122
+ },
123
+ }
124
+ ```
125
+
126
+ ### 2. Customize Colors
127
+
128
+ ```typescript
129
+ export const themeConfig = {
130
+ colors: {
131
+ light: {
132
+ accent: '#0891b2', // Your brand color
133
+ },
134
+ dark: {
135
+ accent: '#22d3ee', // Brighter for dark mode
136
+ },
137
+ },
138
+ }
139
+ ```
140
+
141
+ ### 3. Add Your Logo
142
+
143
+ Place your logo in `public/logo.png` and update `siteConfig.logo.src`.
144
+
145
+ ### 4. Write Documentation
146
+
147
+ Create MDX files in `content/docs/`:
148
+
149
+ ```mdx
150
+ ---
151
+ title: My Page
152
+ description: A description of my page
153
+ ---
154
+
155
+ # Hello World
156
+
157
+ This is my documentation page!
158
+
159
+ <Tip>
160
+ You can use all the built-in components.
161
+ </Tip>
162
+ ```
163
+
164
+ ### 5. Configure Navigation
165
+
166
+ Edit `content/docs/meta.json`:
167
+
168
+ ```json
169
+ {
170
+ "title": "Documentation",
171
+ "pages": [
172
+ "index",
173
+ "quickstart",
174
+ "---Getting Started---",
175
+ "installation",
176
+ "configuration",
177
+ "---Guides---",
178
+ "deployment"
179
+ ]
180
+ }
181
+ ```
182
+
183
+ ## Available Components
184
+
185
+ ### Cards
186
+
187
+ ```mdx
188
+ <CardGroup cols={2}>
189
+ <Card title="Quick Start" icon="rocket" href="/docs/quickstart">
190
+ Get started in 5 minutes
191
+ </Card>
192
+ <Card title="API Reference" icon="code" href="/docs/api">
193
+ Explore our API
194
+ </Card>
195
+ </CardGroup>
196
+ ```
197
+
198
+ ### Callouts
199
+
200
+ ```mdx
201
+ <Info>Informational message</Info>
202
+ <Tip>Helpful tip</Tip>
203
+ <Warning>Warning message</Warning>
204
+ <Note>Additional note</Note>
205
+ <Check>Success message</Check>
206
+ ```
207
+
208
+ ### Steps
209
+
210
+ ```mdx
211
+ <Steps>
212
+ <Step title="Install">
213
+ Run `npm install`
214
+ </Step>
215
+ <Step title="Configure">
216
+ Edit your config file
217
+ </Step>
218
+ </Steps>
219
+ ```
220
+
221
+ ### Tabs
222
+
223
+ ```mdx
224
+ <Tabs>
225
+ <Tab title="npm">npm install package</Tab>
226
+ <Tab title="yarn">yarn add package</Tab>
227
+ </Tabs>
228
+ ```
229
+
230
+ ### Accordions
231
+
232
+ ```mdx
233
+ <AccordionGroup>
234
+ <Accordion title="Question 1">Answer 1</Accordion>
235
+ <Accordion title="Question 2">Answer 2</Accordion>
236
+ </AccordionGroup>
237
+ ```
238
+
239
+ ## Deployment
240
+
241
+ ### Vercel (Recommended)
242
+
243
+ 1. Push to GitHub
244
+ 2. Import at [vercel.com/new](https://vercel.com/new)
245
+ 3. Deploy!
246
+
247
+ ### Netlify
248
+
249
+ 1. Add `netlify.toml`:
250
+ ```toml
251
+ [build]
252
+ command = "npm run build"
253
+ publish = ".next"
254
+ [[plugins]]
255
+ package = "@netlify/plugin-nextjs"
256
+ ```
257
+ 2. Connect and deploy
258
+
259
+ ### Other Hosts
260
+
261
+ Any platform supporting Next.js works. Run `npm run build` and deploy the output.
262
+
263
+ ## Contributing
264
+
265
+ Contributions are welcome! Please feel free to submit a Pull Request.
266
+
267
+ ## License
268
+
269
+ MIT License - feel free to use this for any project.
270
+
271
+ ## Credits
272
+
273
+ Built with:
274
+ - [Next.js](https://nextjs.org)
275
+ - [Fumadocs](https://fumadocs.vercel.app)
276
+ - [Tailwind CSS](https://tailwindcss.com)
277
+
278
+ Inspired by [Mintlify](https://mintlify.com).
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { Info, Tip, Warning, Note, Check } from '../../app/components/docs/mdx/callout'
4
+
5
+ describe('Callout Components', () => {
6
+ describe('Info', () => {
7
+ it('renders children content', () => {
8
+ render(<Info>Test information message</Info>)
9
+ expect(screen.getByText('Test information message')).toBeDefined()
10
+ })
11
+
12
+ it('has accessible structure', () => {
13
+ render(<Info>Accessible content</Info>)
14
+ const callout = screen.getByText('Accessible content').closest('div')
15
+ expect(callout).toBeDefined()
16
+ })
17
+ })
18
+
19
+ describe('Tip', () => {
20
+ it('renders children content', () => {
21
+ render(<Tip>Helpful tip content</Tip>)
22
+ expect(screen.getByText('Helpful tip content')).toBeDefined()
23
+ })
24
+ })
25
+
26
+ describe('Warning', () => {
27
+ it('renders children content', () => {
28
+ render(<Warning>Warning message</Warning>)
29
+ expect(screen.getByText('Warning message')).toBeDefined()
30
+ })
31
+ })
32
+
33
+ describe('Note', () => {
34
+ it('renders children content', () => {
35
+ render(<Note>Note content</Note>)
36
+ expect(screen.getByText('Note content')).toBeDefined()
37
+ })
38
+ })
39
+
40
+ describe('Check', () => {
41
+ it('renders children content', () => {
42
+ render(<Check>Success message</Check>)
43
+ expect(screen.getByText('Success message')).toBeDefined()
44
+ })
45
+ })
46
+ })
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { Card, CardGroup } from '../../app/components/docs/mdx/card'
4
+
5
+ describe('Card Component', () => {
6
+ it('renders with title and children', () => {
7
+ render(
8
+ <Card title="Test Card">
9
+ Card content here
10
+ </Card>
11
+ )
12
+ expect(screen.getByText('Test Card')).toBeDefined()
13
+ expect(screen.getByText('Card content here')).toBeDefined()
14
+ })
15
+
16
+ it('renders as a link when href is provided', () => {
17
+ render(
18
+ <Card title="Linked Card" href="/docs/test">
19
+ Click me
20
+ </Card>
21
+ )
22
+ const link = screen.getByRole('link')
23
+ expect(link).toBeDefined()
24
+ expect(link.getAttribute('href')).toBe('/docs/test')
25
+ })
26
+
27
+ it('renders icon when provided', () => {
28
+ render(
29
+ <Card title="Card with Icon" icon="rocket">
30
+ Content
31
+ </Card>
32
+ )
33
+ // Icon should be rendered (as SVG)
34
+ expect(screen.getByText('Card with Icon')).toBeDefined()
35
+ })
36
+ })
37
+
38
+ describe('CardGroup Component', () => {
39
+ it('renders children cards', () => {
40
+ render(
41
+ <CardGroup cols={2}>
42
+ <Card title="Card 1">Content 1</Card>
43
+ <Card title="Card 2">Content 2</Card>
44
+ </CardGroup>
45
+ )
46
+ expect(screen.getByText('Card 1')).toBeDefined()
47
+ expect(screen.getByText('Card 2')).toBeDefined()
48
+ })
49
+
50
+ it('applies grid layout', () => {
51
+ const { container } = render(
52
+ <CardGroup cols={3}>
53
+ <Card title="Card">Content</Card>
54
+ </CardGroup>
55
+ )
56
+ const grid = container.firstChild
57
+ expect(grid).toBeDefined()
58
+ })
59
+ })
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react'
3
+ import { Tabs, Tab } from '../../app/components/docs/mdx/tabs'
4
+
5
+ describe('Tabs Component', () => {
6
+ afterEach(() => {
7
+ cleanup()
8
+ })
9
+
10
+ it('renders tab titles', () => {
11
+ render(
12
+ <Tabs>
13
+ <Tab title="First">First content</Tab>
14
+ <Tab title="Second">Second content</Tab>
15
+ </Tabs>
16
+ )
17
+ expect(screen.getAllByText('First')[0]).toBeDefined()
18
+ expect(screen.getAllByText('Second')[0]).toBeDefined()
19
+ })
20
+
21
+ it('shows first tab content by default', () => {
22
+ render(
23
+ <Tabs>
24
+ <Tab title="First">First content</Tab>
25
+ <Tab title="Second">Second content</Tab>
26
+ </Tabs>
27
+ )
28
+ expect(screen.getAllByText('First content')[0]).toBeDefined()
29
+ })
30
+
31
+ it('switches tab content when clicked', () => {
32
+ render(
33
+ <Tabs>
34
+ <Tab title="First">First content</Tab>
35
+ <Tab title="Second">Second content</Tab>
36
+ </Tabs>
37
+ )
38
+
39
+ // Click on second tab
40
+ const secondTabs = screen.getAllByText('Second')
41
+ fireEvent.click(secondTabs[0])
42
+
43
+ // Second content should now be visible
44
+ expect(screen.getAllByText('Second content')[0]).toBeDefined()
45
+ })
46
+ })
47
+
48
+ describe('Tab Component', () => {
49
+ afterEach(() => {
50
+ cleanup()
51
+ })
52
+
53
+ it('renders children when active', () => {
54
+ render(
55
+ <Tabs>
56
+ <Tab title="Test">Tab content here</Tab>
57
+ </Tabs>
58
+ )
59
+ expect(screen.getByText('Tab content here')).toBeDefined()
60
+ })
61
+ })
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { siteConfig, themeConfig, getCSSVariables } from '../lib/theme-config'
3
+
4
+ describe('theme-config', () => {
5
+ describe('siteConfig', () => {
6
+ it('should have required fields', () => {
7
+ expect(siteConfig.name).toBeDefined()
8
+ expect(siteConfig.description).toBeDefined()
9
+ expect(siteConfig.url).toBeDefined()
10
+ })
11
+
12
+ it('should have logo configuration', () => {
13
+ expect(siteConfig.logo).toBeDefined()
14
+ expect(siteConfig.logo.src).toBeDefined()
15
+ expect(siteConfig.logo.width).toBeGreaterThan(0)
16
+ expect(siteConfig.logo.height).toBeGreaterThan(0)
17
+ })
18
+ })
19
+
20
+ describe('themeConfig', () => {
21
+ it('should have light and dark color schemes', () => {
22
+ expect(themeConfig.colors.light).toBeDefined()
23
+ expect(themeConfig.colors.dark).toBeDefined()
24
+ })
25
+
26
+ it('should have accent colors', () => {
27
+ expect(themeConfig.colors.light.accent).toBeDefined()
28
+ expect(themeConfig.colors.dark.accent).toBeDefined()
29
+ })
30
+
31
+ it('should have OG image configuration', () => {
32
+ expect(themeConfig.ogImage).toBeDefined()
33
+ expect(themeConfig.ogImage.gradient).toBeDefined()
34
+ expect(themeConfig.ogImage.titleColor).toBeDefined()
35
+ })
36
+ })
37
+
38
+ describe('getCSSVariables', () => {
39
+ it('should return CSS variables for light mode', () => {
40
+ const vars = getCSSVariables('light')
41
+ expect(vars['--accent']).toBe(themeConfig.colors.light.accent)
42
+ })
43
+
44
+ it('should return CSS variables for dark mode', () => {
45
+ const vars = getCSSVariables('dark')
46
+ expect(vars['--accent']).toBe(themeConfig.colors.dark.accent)
47
+ })
48
+ })
49
+ })
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { cn } from '../lib/utils'
3
+
4
+ describe('cn utility', () => {
5
+ it('should merge class names', () => {
6
+ expect(cn('foo', 'bar')).toBe('foo bar')
7
+ })
8
+
9
+ it('should handle conditional classes', () => {
10
+ expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz')
11
+ })
12
+
13
+ it('should merge Tailwind classes correctly', () => {
14
+ expect(cn('px-4', 'px-6')).toBe('px-6')
15
+ expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500')
16
+ })
17
+
18
+ it('should handle arrays', () => {
19
+ expect(cn(['foo', 'bar'])).toBe('foo bar')
20
+ })
21
+
22
+ it('should handle undefined and null', () => {
23
+ expect(cn('foo', undefined, null, 'bar')).toBe('foo bar')
24
+ })
25
+ })
@@ -0,0 +1,90 @@
1
+ import { ImageResponse } from 'next/og'
2
+ import { NextRequest } from 'next/server'
3
+ import { themeConfig, siteConfig } from '@/lib/theme-config'
4
+
5
+ export const runtime = 'edge'
6
+
7
+ export async function GET(request: NextRequest) {
8
+ const { searchParams } = new URL(request.url)
9
+ let title = searchParams.get('title') || 'Documentation'
10
+ const section = searchParams.get('section') || siteConfig.name
11
+
12
+ // Truncate very long titles to prevent overflow
13
+ if (title.length > 60) {
14
+ title = title.slice(0, 57) + '...'
15
+ }
16
+
17
+ // Determine font size based on title length
18
+ const fontSize = title.length > 40 ? 48 : title.length > 25 ? 56 : 64
19
+
20
+ return new ImageResponse(
21
+ (
22
+ <div
23
+ style={{
24
+ display: 'flex',
25
+ flexDirection: 'column',
26
+ width: '100%',
27
+ height: '100%',
28
+ background: themeConfig.ogImage.gradient,
29
+ padding: '60px',
30
+ }}
31
+ >
32
+ {/* Logo placeholder */}
33
+ <div style={{ display: 'flex', alignItems: 'flex-start' }}>
34
+ <div
35
+ style={{
36
+ display: 'flex',
37
+ width: '48px',
38
+ height: '48px',
39
+ background: `linear-gradient(135deg, ${themeConfig.colors.dark.accent} 0%, ${themeConfig.colors.light.accent} 100%)`,
40
+ borderRadius: '12px',
41
+ alignItems: 'center',
42
+ justifyContent: 'center',
43
+ }}
44
+ >
45
+ <span style={{ fontSize: '24px', color: '#fff', fontWeight: 700 }}>
46
+ {siteConfig.name.charAt(0)}
47
+ </span>
48
+ </div>
49
+ </div>
50
+
51
+ {/* Section name */}
52
+ <div
53
+ style={{
54
+ display: 'flex',
55
+ marginTop: 'auto',
56
+ }}
57
+ >
58
+ <span
59
+ style={{
60
+ color: themeConfig.ogImage.sectionColor,
61
+ fontSize: '28px',
62
+ fontWeight: 500,
63
+ }}
64
+ >
65
+ {section}
66
+ </span>
67
+ </div>
68
+
69
+ {/* Title */}
70
+ <div
71
+ style={{
72
+ fontSize: `${fontSize}px`,
73
+ fontWeight: 700,
74
+ color: themeConfig.ogImage.titleColor,
75
+ marginTop: '16px',
76
+ lineHeight: 1.1,
77
+ maxWidth: '100%',
78
+ overflow: 'hidden',
79
+ }}
80
+ >
81
+ {title}
82
+ </div>
83
+ </div>
84
+ ),
85
+ {
86
+ width: 1200,
87
+ height: 630,
88
+ }
89
+ )
90
+ }
@@ -0,0 +1,6 @@
1
+ import { source } from '@/lib/docs-source'
2
+ import { createFromSource } from 'fumadocs-core/search/server'
3
+
4
+ export const { GET } = createFromSource(source, {
5
+ language: 'english',
6
+ })
@@ -0,0 +1,41 @@
1
+ import Link from 'next/link'
2
+
3
+ interface DocsPagerProps {
4
+ previous?: { name: string; url: string }
5
+ next?: { name: string; url: string }
6
+ }
7
+
8
+ export function DocsPager({ previous, next }: DocsPagerProps) {
9
+ if (!previous && !next) return null
10
+
11
+ return (
12
+ <nav className="flex justify-between mt-16 pt-8 border-t border-border">
13
+ {previous ? (
14
+ <Link
15
+ href={previous.url}
16
+ className="group flex flex-col gap-1 max-w-[45%]"
17
+ >
18
+ <span className="text-sm text-muted-foreground">Previous</span>
19
+ <span className="text-foreground font-medium group-hover:text-[var(--accent)] transition-colors">
20
+ ← {previous.name}
21
+ </span>
22
+ </Link>
23
+ ) : (
24
+ <div />
25
+ )}
26
+ {next ? (
27
+ <Link
28
+ href={next.url}
29
+ className="group flex flex-col gap-1 items-end text-right max-w-[45%]"
30
+ >
31
+ <span className="text-sm text-muted-foreground">Next</span>
32
+ <span className="text-foreground font-medium group-hover:text-[var(--accent)] transition-colors">
33
+ {next.name} →
34
+ </span>
35
+ </Link>
36
+ ) : (
37
+ <div />
38
+ )}
39
+ </nav>
40
+ )
41
+ }