create-next-mdx-blog-app 2.0.0 → 2.1.1

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.
package/.env.example CHANGED
@@ -1,6 +1,17 @@
1
- # Supabase — required for dynamic blog post fetching
2
- SUPABASE_URL=
3
- SUPABASE_ANON_KEY=
4
-
5
- # Anthropic — required for the Blog Assistant chat feature
6
- ANTHROPIC_API_KEY=
1
+ # Supabase — required for dynamic blog post fetching
2
+ SUPABASE_URL=
3
+ SUPABASE_ANON_KEY=
4
+
5
+ # Anthropic — required for the Blog Assistant chat feature
6
+ ANTHROPIC_API_KEY=
7
+
8
+ # GitHub — optional but recommended for the <GitHubGist> component
9
+ # Without a token, the GitHub API allows 60 requests/hr per IP.
10
+ # With a token (read-only gist scope), the limit rises to 5,000 requests/hr.
11
+ GITHUB_TOKEN=
12
+
13
+ # GitHub username used to construct mdxgists.net URLs (defaults to CodingAbdullah)
14
+ GITHUB_USERNAME=
15
+
16
+ # Base URL for mdxgists.net links (defaults to https://mdxgists.net)
17
+ GIST_BASE_URL=
package/README.md CHANGED
@@ -59,7 +59,7 @@ npx create-next-mdx-blog-app .
59
59
  ### Package Information
60
60
 
61
61
  - **Package Name**: `create-next-mdx-blog-app`
62
- - **Version**: `2.0.0`
62
+ - **Version**: `2.1.1`
63
63
  - **License**: MIT
64
64
  - **Homepage**: [https://www.npmjs.com/package/create-next-mdx-blog-app](https://www.npmjs.com/package/create-next-mdx-blog-app/)
65
65
 
@@ -83,7 +83,7 @@ npx create-next-mdx-blog-app .
83
83
  - **@ai-sdk/anthropic**: Anthropic provider for the Vercel AI SDK, used to connect to Claude models.
84
84
  - **@ai-sdk/react**: React hooks (`useChat`) for building streaming AI chat interfaces.
85
85
  - **zod**: TypeScript-first schema validation library used to define and validate AI tool parameters.
86
- - **sonner**: Lightweight toast notification library used for user feedback in the Code Sandbox.
86
+ - **sonner**: Lightweight toast notification library used for user feedback across the Code Sandbox, the `CodeBlock` copy button, and the GitHub Gist copy button.
87
87
 
88
88
  ## 🌐 Static/Dynamic Rendering with MDX
89
89
  This project utilizes MDX for both static and dynamic rendering of blog posts. The two MDX files included in the project serve as examples of how to structure your content.
@@ -111,19 +111,56 @@ The project includes a custom `CodeBlock` component for syntax highlighting code
111
111
 
112
112
  Default theme is set to the `vscDarkPlus` theme. Feel free to modify the theme and even add your own syntax highlighting library if you so choose.
113
113
 
114
- You can read more about the library used in this project <a href="https://react-syntax-highlighter.github.io/react-syntax-highlighter/demo/">here.</a>
114
+ A **copy button** is rendered in the top-right corner of every code block. Clicking it writes the code to the clipboard and triggers a sonner toast notification confirming success or failure.
115
115
 
116
+ You can read more about the library used in this project <a href="https://react-syntax-highlighter.github.io/react-syntax-highlighter/demo/">here.</a>
116
117
 
117
118
  ### GitHub Gists
118
- For safety and ease of use, GitHub Gists can be integrated into MDX files to manage code snippets with its own custom component.
119
+ GitHub Gists can be embedded directly in MDX files using the `<GitHubGist>` custom component. The component fetches gist content live from the **GitHub REST API** at render time and caches the result for one hour via Next.js ISR (`next: { revalidate: 3600 }`).
120
+
121
+ ```mdx
122
+ <GitHubGist id="your-gist-id" figCaptionText="Caption shown below the gist" />
123
+ ```
124
+
125
+ #### How it works
126
+ 1. `GitHubGist` calls `https://api.github.com/gists/<id>` to retrieve metadata (file name, detected language).
127
+ 2. It then fetches the raw file content from the `raw_url` returned by the API.
128
+ 3. The language is mapped to a Prism-compatible token using the built-in `GITHUB_GIST_LANGUAGE_MAP` constant (70+ languages supported — see `src/utils/constants/GitHubGistConstants.ts`).
129
+ 4. The code is rendered via `GistCodeBlock` (syntax-highlighted with `react-syntax-highlighter`) inside a scrollable container with a custom green scrollbar (`scrollbar-gist`).
130
+
131
+ #### mdxgists.net
132
+ Each rendered gist includes a header bar with:
133
+ - The detected **language badge**
134
+ - A **Copy** button that writes the raw content to clipboard with a sonner toast confirmation
135
+ - A **mdxgists.net** link that opens the gist on [mdxgists.net](https://mdxgists.net) — a companion site for browsing and sharing MDX-ready gists
136
+
137
+ The mdxgists.net URL is constructed as `<GIST_BASE_URL>/<GITHUB_USERNAME>/<gist-id>` using values from `GitHubGistConstants.ts`. Both values can be overridden via environment variables (see below).
138
+
139
+ #### Components
140
+ - `src/components/customMDXComponents/GitHubGist.tsx` — async Server Component; fetches from GitHub API, resolves language, builds the mdxgists.net URL, renders the full gist UI
141
+ - `src/components/customMDXComponents/GistCodeBlock.tsx` — syntax-highlighted code display using `react-syntax-highlighter`
142
+ - `src/components/customMDXComponents/GistCopyButton.tsx` — `"use client"` component; copy-to-clipboard with sonner toast + mdxgists.net external link
143
+
144
+ #### Language Detection
145
+ The `GITHUB_GIST_LANGUAGE_MAP` in `src/utils/constants/GitHubGistConstants.ts` maps GitHub's reported language names to Prism-compatible identifiers. Over 70 languages are supported out of the box including TypeScript, Python, Rust, Go, Solidity, YAML, SQL, and more. If a language is not in the map, the component falls back to `language.toLowerCase()`. If no language is detected, it falls back to `text`.
146
+
147
+ #### GitHub OAuth Token (optional but recommended)
148
+ Unauthenticated requests to the GitHub API are rate-limited to **60 requests per hour** per IP. For production use, set a `GITHUB_TOKEN` environment variable to increase this to **5,000 requests per hour**:
119
149
 
120
- Note that only GitHub Gists that are publicly available are supported. You can modify the component to integrate private Gists.
150
+ ```bash
151
+ # .env.local
152
+ GITHUB_TOKEN=ghp_your_personal_access_token
153
+ ```
121
154
 
122
- This custom component also utilizes the `react-syntax-highlighter` package for syntax highlighting the publicly accessible GitHub gists.
155
+ To generate a token: GitHub → Settings → Developer settings → Personal access tokens → Generate new token. The token only needs the **read-only** `gist` scope (or no scopes at all for public gists).
156
+
157
+ > Only **public** gists are supported by default. The component can be modified to support private gists by adding the appropriate scopes to the token.
123
158
 
124
159
  ### MDX Images
125
160
  The project comes with its own `MDXImage` component that utilizes the Next.js built-in `Image` component as well as the built-in `figure` and `figcaption` elements to integrate imaging and captions seamlessly.
126
161
 
162
+ Images are displayed inside a styled container with a **green glow border** and a **hover scale effect** consistent with the rest of the matrix-green design system.
163
+
127
164
  ## 🖥️ Code Sandbox
128
165
  The project includes an interactive in-browser code execution environment powered by **Sandpack** (<b>route</b>: `/code-sandbox`).
129
166
 
@@ -187,12 +224,18 @@ cp .env.example .env.local
187
224
  | `SUPABASE_URL` | Dynamic blog post fetching (Supabase client) |
188
225
  | `SUPABASE_ANON_KEY` | Dynamic blog post fetching (Supabase client) |
189
226
  | `ANTHROPIC_API_KEY` | Blog Assistant chatbot (`/api/chat` edge route) |
227
+ | `GITHUB_TOKEN` | GitHub API authentication for `<GitHubGist>` — optional but raises rate limit from 60 to 5,000 req/hr |
228
+ | `GITHUB_USERNAME` | Your GitHub username, used to construct the mdxgists.net URL (defaults to `CodingAbdullah`) |
229
+ | `GIST_BASE_URL` | Base URL for mdxgists.net links (defaults to `https://mdxgists.net`) |
190
230
 
191
231
  ```
192
232
  # .env.local
193
233
  SUPABASE_URL=
194
234
  SUPABASE_ANON_KEY=
195
235
  ANTHROPIC_API_KEY=
236
+ GITHUB_TOKEN=
237
+ GITHUB_USERNAME=
238
+ GIST_BASE_URL=
196
239
  ```
197
240
 
198
241
  ## 🌩️ AWS
@@ -210,7 +253,7 @@ docker build -t mdx-medium-blog .
210
253
  ``
211
254
 
212
255
  ``
213
- docker run -e SUPABASE_URL=your_supabase_url \ -e SUPABASE_ANON_KEY=your_supabase_anon_key \ -e ANTHROPIC_API_KEY=your_anthropic_api_key \ -p 3000:3000 mdx-medium-blog
256
+ docker run -e SUPABASE_URL=your_supabase_url \ -e SUPABASE_ANON_KEY=your_supabase_anon_key \ -e ANTHROPIC_API_KEY=your_anthropic_api_key \ -e GITHUB_TOKEN=your_github_token \ -e GITHUB_USERNAME=your_github_username \ -e GIST_BASE_URL=https://mdxgists.net \ -p 3000:3000 mdx-medium-blog
214
257
  ``
215
258
 
216
259
  ## 🔄 CRUD Operations and Supabase Actions
@@ -227,7 +270,7 @@ The `/scripts` folder contains various scripts to help set up the project and da
227
270
  - **Bash Shell Script**: Script for setting up project on Linux, Mac, etc.
228
271
 
229
272
  ### GitHub Gist Fetcher
230
- The project includes a GitHub Gist fetcher script that downloads public GitHub Gist content and exports it to a text file for local use.
273
+ > **Note:** This script is no longer required for rendering gists in MDX. The `<GitHubGist>` component now fetches content live from the GitHub API at render time. This script is retained for offline inspection or local caching workflows.
231
274
 
232
275
  **Location**: `/scripts/github-gist-fetcher/fetch-github-gist.ts`
233
276
 
@@ -270,6 +313,29 @@ The article slug is the name of the markdown file located in the `/src/markdown`
270
313
 
271
314
  The update statement takes in an additional parameter which is also the same thing (file name minus the `.mdx` extension located in the `/src/markdown` directory).
272
315
 
316
+ ## 📱 Mobile Responsiveness
317
+ All components in the starter kit are built mobile-first using Tailwind CSS responsive prefixes (`sm:`, `md:`, `lg:`).
318
+
319
+ Key responsive behaviours:
320
+
321
+ | Component | Mobile behaviour |
322
+ |---|---|
323
+ | `ArticleHeader` | Title scales from `text-2xl` → `text-4xl`; author row stacks vertically on small screens |
324
+ | `ArticleAuthorBio` | Avatar and text stack vertically; button goes full-width; text is center-aligned on mobile |
325
+ | `GitHubGist` | Header wraps via `flex-wrap`; code area scrolls horizontally with a styled green scrollbar |
326
+ | `CodeBlock` | Copy button is absolutely positioned and does not interfere with code flow |
327
+ | `MDXImage` | Image container scales inline; hover effect applies on capable devices |
328
+ | Chat & Code Sandbox | Responsive padding and height calculations across all breakpoints |
329
+
330
+ The root layout exports a `viewport` configuration (via Next.js `Viewport` type) ensuring correct scaling on all mobile browsers:
331
+
332
+ ```ts
333
+ export const viewport: Viewport = {
334
+ width: 'device-width',
335
+ initialScale: 1,
336
+ };
337
+ ```
338
+
273
339
  ## 📊 Analytics
274
340
  Integrated in this setup project is Vercel Analytics (`@vercel/analytics`) to track user interactions and performance metrics of the blog.
275
341
 
package/mdx.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module '*.mdx' {
2
+ import type { ComponentType } from 'react';
3
+ const component: ComponentType;
4
+ export default component;
5
+ }
package/next.config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import createMDX from '@next/mdx';
2
- import type { NextConfig } from 'next';
2
+ import type { NextConfig } from 'next';
3
3
 
4
4
  // Adding acceptable page extensions to be used in this application
5
5
  // Adding external domains to be used in this application
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-next-mdx-blog-app",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "bin": {
5
5
  "create-next-mdx-blog-app": "./bin/create.js"
6
6
  },
@@ -29,46 +29,46 @@
29
29
  "lint": "next lint"
30
30
  },
31
31
  "dependencies": {
32
- "@ai-sdk/anthropic": "^3.0.0",
33
- "@ai-sdk/react": "^3.0.99",
34
- "@codesandbox/sandpack-react": "^2.13.5",
32
+ "@ai-sdk/anthropic": "^3.0.64",
33
+ "@ai-sdk/react": "^3.0.143",
34
+ "@codesandbox/sandpack-react": "^2.20.0",
35
35
  "@mdx-js/loader": "^3.1.1",
36
36
  "@mdx-js/react": "^3.1.1",
37
- "@next/mdx": "^16.1.1",
38
- "@radix-ui/react-avatar": "^1.1.10",
39
- "@radix-ui/react-slot": "^1.2.3",
40
- "@supabase/supabase-js": "^2.89.0",
37
+ "@next/mdx": "^16.2.1",
38
+ "@radix-ui/react-avatar": "^1.1.11",
39
+ "@radix-ui/react-slot": "^1.2.4",
40
+ "@supabase/supabase-js": "^2.101.0",
41
41
  "@types/mdx": "^2.0.13",
42
42
  "@types/react-syntax-highlighter": "^15.5.13",
43
- "@vercel/analytics": "^1.6.1",
44
- "ai": "^6.0.97",
43
+ "@vercel/analytics": "^2.0.1",
44
+ "ai": "^6.0.141",
45
45
  "class-variance-authority": "^0.7.1",
46
46
  "clsx": "^2.1.1",
47
47
  "degit": "^2.8.4",
48
- "execa": "^9.6.0",
48
+ "execa": "^9.6.1",
49
49
  "gray-matter": "^4.0.3",
50
- "inquirer": "^8.2.6",
51
- "lucide-react": "^0.562.0",
52
- "next": "^16.1.1",
50
+ "inquirer": "^13.3.2",
51
+ "lucide-react": "^1.7.0",
52
+ "next": "^16.2.1",
53
53
  "next-mdx-remote": "^6.0.0",
54
- "react": "^19.2.3",
55
- "react-dom": "^19.2.3",
56
- "react-syntax-highlighter": "^15.6.1",
57
- "sonner": "^1.7.4",
58
- "tailwind-merge": "^3.3.0",
59
- "tsx": "^4.19.4",
60
- "zod": "^3.24.0"
54
+ "react": "^19.2.4",
55
+ "react-dom": "^19.2.4",
56
+ "react-syntax-highlighter": "^16.1.1",
57
+ "sonner": "^2.0.7",
58
+ "tailwind-merge": "^3.5.0",
59
+ "tsx": "^4.21.0",
60
+ "zod": "^4.3.6"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@eslint/eslintrc": "^3",
64
64
  "@tailwindcss/postcss": "^4",
65
- "@types/node": "^20.17.58",
65
+ "@types/node": "^25.5.0",
66
66
  "@types/react": "^19",
67
67
  "@types/react-dom": "^19",
68
- "eslint": "^9",
69
- "eslint-config-next": "15.3.2",
68
+ "eslint": "^10",
69
+ "eslint-config-next": "16.2.1",
70
70
  "tailwindcss": "^4",
71
- "tw-animate-css": "^1.3.2",
72
- "typescript": "5.9.3"
71
+ "tw-animate-css": "^1.4.0",
72
+ "typescript": "6.0.2"
73
73
  }
74
74
  }
@@ -174,4 +174,51 @@
174
174
  .matrix-scrollbar::-webkit-scrollbar-thumb:hover {
175
175
  background: rgba(0, 200, 0, 0.5);
176
176
  }
177
+
178
+ /* Enhanced scrollbar for GitHub gist component */
179
+ .scrollbar-gist {
180
+ scrollbar-width: thin;
181
+ scrollbar-color: rgba(34, 197, 94, 0.7) rgba(15, 23, 42, 0.8);
182
+ }
183
+
184
+ .scrollbar-gist::-webkit-scrollbar {
185
+ width: 10px;
186
+ height: 10px;
187
+ }
188
+
189
+ .scrollbar-gist::-webkit-scrollbar-track {
190
+ background: rgba(15, 23, 42, 0.9);
191
+ border-radius: 6px;
192
+ border: 1px solid rgba(34, 197, 94, 0.2);
193
+ box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.5);
194
+ }
195
+
196
+ .scrollbar-gist::-webkit-scrollbar-thumb {
197
+ background: linear-gradient(135deg,
198
+ rgba(34, 197, 94, 0.9) 0%,
199
+ rgba(22, 163, 74, 0.8) 30%,
200
+ rgba(21, 128, 61, 0.7) 70%,
201
+ rgba(34, 197, 94, 0.9) 100%);
202
+ border-radius: 6px;
203
+ border: 1px solid rgba(34, 197, 94, 0.4);
204
+ box-shadow: 0 0 8px rgba(34, 197, 94, 0.5),
205
+ inset 0 1px 0 rgba(34, 197, 94, 0.3);
206
+ transition: all 0.3s ease;
207
+ }
208
+
209
+ .scrollbar-gist::-webkit-scrollbar-thumb:hover {
210
+ background: linear-gradient(135deg,
211
+ rgba(34, 197, 94, 1) 0%,
212
+ rgba(22, 163, 74, 0.9) 30%,
213
+ rgba(21, 128, 61, 0.8) 70%,
214
+ rgba(34, 197, 94, 1) 100%);
215
+ box-shadow: 0 0 12px rgba(34, 197, 94, 0.7),
216
+ inset 0 1px 0 rgba(34, 197, 94, 0.4);
217
+ transform: scale(1.05);
218
+ }
219
+
220
+ .scrollbar-gist::-webkit-scrollbar-corner {
221
+ background: rgba(15, 23, 42, 0.9);
222
+ border: 1px solid rgba(34, 197, 94, 0.2);
223
+ }
177
224
  }
@@ -1,6 +1,12 @@
1
1
  import "@/app/globals.css";
2
2
  import { Analytics } from "@vercel/analytics/next";
3
3
  import { Toaster } from "sonner";
4
+ import type { Viewport } from "next";
5
+
6
+ export const viewport: Viewport = {
7
+ width: "device-width",
8
+ initialScale: 1,
9
+ };
4
10
 
5
11
  // Root Layout for the simple Next.js application
6
12
  export default function RootLayout({
@@ -7,16 +7,16 @@ import type { ArticleAuthorInfoType } from "@/utils/types";
7
7
  // Article Author Bio Section component
8
8
  export default function ArticleAuthorBio({ authorInformation }: { authorInformation: ArticleAuthorInfoType }) {
9
9
  return (
10
- <div className="glass-card p-6 mb-12">
11
- <div className="flex items-start">
12
- <Avatar className="h-16 w-16 mr-6 border-2 border-green-500/30">
10
+ <div className="glass-card p-4 sm:p-6 mb-8 sm:mb-12">
11
+ <div className="flex flex-col sm:flex-row items-center sm:items-start space-y-4 sm:space-y-0">
12
+ <Avatar className="mr-0 sm:mr-6 mb-4 sm:mb-0 border-2 border-green-500/30">
13
13
  <Image src={authorInformation.authorProfileImageURL} alt="No Name" width={64} height={64} className="rounded-full object-cover" />
14
14
  </Avatar>
15
- <div>
16
- <h3 className="text-xl font-semibold mb-2 matrix-glow text-green-300">{authorInformation.authorName}</h3>
17
- <p className="text-green-200/80 mb-3">{authorInformation.authorDescription}</p>
15
+ <div className="text-center sm:text-left w-full">
16
+ <h3 className="text-lg sm:text-xl font-semibold mb-2 matrix-glow text-green-300">{authorInformation.authorName}</h3>
17
+ <p className="text-green-200/80 mb-3 text-sm sm:text-base">{authorInformation.authorDescription}</p>
18
18
  <Link href="/">
19
- <Button className="bg-green-600 hover:bg-green-700 text-white border-none">
19
+ <Button className="bg-green-600 hover:bg-green-700 text-white border-none w-full sm:w-auto text-sm sm:text-base">
20
20
  Go Back To Home Page
21
21
  </Button>
22
22
  </Link>
@@ -11,26 +11,28 @@ export default function ArticleHeader({ articleHeaderInformation } : { articleHe
11
11
  <Badge className="mb-3 bg-green-900/60 text-green-100 border border-green-500/50">
12
12
  { articleHeaderInformation.articleTags[0] }
13
13
  </Badge>
14
- <h1 className="text-3xl md:text-5xl font-bold mb-6 matrix-glow leading-tight text-green-300">
14
+ <h1 className="text-2xl sm:text-3xl md:text-4xl font-bold mb-4 sm:mb-6 matrix-glow leading-tight text-green-300">
15
15
  { articleHeaderInformation.articleTitle }
16
16
  </h1>
17
- <p className="text-xl text-green-200/80 mb-6 leading-relaxed">
17
+ <p className="text-base sm:text-xl text-green-200/80 mb-4 sm:mb-6 leading-relaxed">
18
18
  { articleHeaderInformation.articleDescription }
19
- </p>
19
+ </p>
20
20
  {/* Author info and publish date */}
21
- <div className="flex items-center mb-6">
22
- <Avatar className="h-12 w-12 mr-4 border-2 border-green-500/30">
23
- <Image src={ articleHeaderInformation.articleAuthorInfo.authorProfileImageURL } alt="No Name Exists" width={48} height={48} className="rounded-full object-cover" />
24
- </Avatar>
25
- <div>
26
- <p className="font-medium text-green-300">{ articleHeaderInformation.articleAuthorInfo.authorName }</p>
27
- <p className="text-sm text-green-400/70">
28
- Published on {new Date(Date.now()).toLocaleDateString('en-US', {
29
- year: 'numeric',
30
- month: 'long',
31
- day: 'numeric'
32
- })} • { articleHeaderInformation.articleReadingTime }
33
- </p>
21
+ <div className="flex flex-col sm:flex-row sm:items-center mb-4 sm:mb-6 space-y-2 sm:space-y-0">
22
+ <div className="flex items-center">
23
+ <Avatar className="h-10 w-10 sm:h-12 sm:w-12 mr-3 sm:mr-4 border-2 border-green-500/30">
24
+ <Image src={ articleHeaderInformation.articleAuthorInfo.authorProfileImageURL } alt="No Name Exists" width={48} height={48} className="rounded-full object-cover" />
25
+ </Avatar>
26
+ <div>
27
+ <p className="font-medium text-green-300 text-sm sm:text-base">{ articleHeaderInformation.articleAuthorInfo.authorName }</p>
28
+ <p className="text-xs sm:text-sm text-green-400/70">
29
+ Published on {new Date(Date.now()).toLocaleDateString('en-US', {
30
+ year: 'numeric',
31
+ month: 'long',
32
+ day: 'numeric'
33
+ })} • { articleHeaderInformation.articleReadingTime }
34
+ </p>
35
+ </div>
34
36
  </div>
35
37
  </div>
36
38
  {/* Cover image */}
@@ -1,19 +1,64 @@
1
- import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
2
- import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
3
-
4
- // Custom code block component for handling code in MDX files
5
- // Visual Studio Code Dark Plus theme
6
- const CodeBlock = ({ className = '', children }: { className?: string; children: string }) => {
7
- const match = /language-(\w+)/.exec(className || '')
8
- return (
9
- <SyntaxHighlighter
10
- language={match?.[1] || 'text'}
11
- style={vscDarkPlus}
12
- className="rounded-lg"
13
- >
14
- {String(children).trim()}
15
- </SyntaxHighlighter>
16
- )
17
- }
18
-
19
- export default CodeBlock;
1
+ "use client";
2
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
4
+ import { toast } from 'sonner';
5
+
6
+ // Custom code block component for handling code in MDX files
7
+ // Visual Studio Code Dark Plus theme with copy functionality
8
+ const CodeBlock = ({ className = '', children }: { className?: string; children: string }) => {
9
+ const match = /language-(\w+)/.exec(className || '');
10
+ const language = match?.[1] || 'text';
11
+ const code = String(children).trim();
12
+
13
+ // Copy content and receive a toast message based on action
14
+ const copyToClipboard = async () => {
15
+ try {
16
+ await navigator.clipboard.writeText(code);
17
+ toast.success('Code copied!', {
18
+ style: {
19
+ background: '#0d1117',
20
+ border: '1px solid #22c55e',
21
+ color: 'white'
22
+ }
23
+ });
24
+ }
25
+ catch {
26
+ toast.error('Failed to copy code', {
27
+ style: {
28
+ background: '#0d1117',
29
+ border: '1px solid #22c55e',
30
+ color: 'white'
31
+ }
32
+ });
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div className="relative my-6 group">
38
+ <div className="absolute top-3 right-3 z-10">
39
+ <button
40
+ onClick={copyToClipboard}
41
+ className="flex items-center gap-1 text-xs text-green-400/70 hover:text-green-300 transition-colors duration-200 bg-black/60 hover:bg-black/80 px-2 py-1 rounded border border-green-500/20 backdrop-blur-sm"
42
+ >
43
+ <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
44
+ <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
45
+ <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
46
+ </svg>
47
+ Copy
48
+ </button>
49
+ </div>
50
+ <SyntaxHighlighter
51
+ language={language}
52
+ style={vscDarkPlus}
53
+ className="rounded-lg"
54
+ customStyle={{
55
+ paddingTop: '2rem',
56
+ }}
57
+ >
58
+ {code}
59
+ </SyntaxHighlighter>
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default CodeBlock;
@@ -0,0 +1,19 @@
1
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
2
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
3
+
4
+ interface GistCodeBlockProps {
5
+ content: string;
6
+ language: string;
7
+ }
8
+
9
+ export default function GistCodeBlock({ content, language }: GistCodeBlockProps) {
10
+ return (
11
+ <SyntaxHighlighter
12
+ language={language}
13
+ style={vscDarkPlus}
14
+ customStyle={{ margin: 0, background: 'transparent', fontSize: '0.875rem' }}
15
+ >
16
+ {content}
17
+ </SyntaxHighlighter>
18
+ );
19
+ }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { toast } from 'sonner';
3
+
4
+ interface GistCopyButtonProps {
5
+ content: string;
6
+ mdxGHGistURL: string;
7
+ }
8
+
9
+ export default function GistCopyButton({ content, mdxGHGistURL }: GistCopyButtonProps) {
10
+ const copyToClipboard = async () => {
11
+ try {
12
+ await navigator.clipboard.writeText(content);
13
+ toast.success('GitHub Gist copied!', {
14
+ style: {
15
+ background: '#0d1117',
16
+ border: '1px solid #22c55e',
17
+ color: 'white'
18
+ }
19
+ });
20
+ }
21
+ catch {
22
+ toast.error('Failed to copy GitHub Gist', {
23
+ style: {
24
+ background: '#0d1117',
25
+ border: '1px solid #22c55e',
26
+ color: 'white'
27
+ }
28
+ });
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="flex items-center gap-2 shrink-0">
34
+ <button
35
+ onClick={copyToClipboard}
36
+ className="flex items-center gap-1 text-xs text-green-400/70 hover:text-green-300 transition-colors duration-200 bg-green-500/10 hover:bg-green-500/20 px-2 py-1 rounded border border-green-500/20"
37
+ >
38
+ <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
39
+ <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
40
+ <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
41
+ </svg>
42
+ Copy
43
+ </button>
44
+ <a
45
+ href={mdxGHGistURL}
46
+ target="_blank"
47
+ rel="noopener noreferrer"
48
+ className="flex items-center gap-1 text-xs text-green-400/70 hover:text-green-300 transition-colors duration-200 bg-green-500/10 hover:bg-green-500/20 px-2 py-1 rounded border border-green-500/20"
49
+ >
50
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
52
+ </svg>
53
+ mdxgists.net
54
+ </a>
55
+ </div>
56
+ );
57
+ }
@@ -1,44 +1,84 @@
1
- // components/customMDXComponents/GitHubGist.tsx
2
- import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3
- import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
4
- import type { GitHubGistType } from '@/utils/types';
5
- import fs from 'fs';
6
- import path from 'path';
7
-
8
- // GitHub Gist custom component
9
- // Read the requested GitHub Gist content from local file using its ID
10
- async function GitHubGist({ id, figCaptionText }: GitHubGistType) {
11
- let content = '';
12
- let error = null;
13
-
14
- // Run a try-catch to ensure errors are caught and handled gracefully
15
- try {
16
- const filePath = path.join(process.cwd(), 'src', 'github_gists', `${id}.txt`);
17
-
18
- if (!fs.existsSync(filePath)) {
19
- throw new Error("GitHub gist could not load");
20
- }
21
-
22
- content = fs.readFileSync(filePath, 'utf8');
23
- }
24
- catch {
25
- error = "GitHub gist could not load";
26
- }
27
-
28
- // Show error state
29
- if (error) {
30
- return <div className="text-red-600 bg-red-100 p-4 rounded-lg">{error}</div>;
31
- }
32
-
33
- // Render the GitHub Gist component using React-Syntax-Highlighting
34
- return (
35
- <figure className='text-center'>
36
- <SyntaxHighlighter language="javascript" style={vscDarkPlus}>
37
- {content}
38
- </SyntaxHighlighter>
39
- <figcaption className="mb-4 leading-relaxed text-green-200/90">{figCaptionText}</figcaption>
40
- </figure>
41
- );
42
- }
43
-
44
- export default GitHubGist;
1
+ import type GitHubGistType from '@/utils/types/GitHubGistType';
2
+ import GistCopyButton from './GistCopyButton';
3
+ import GistCodeBlock from './GistCodeBlock';
4
+ import { GITHUB_USERNAME, GITHUB_GIST_LANGUAGE_MAP, GIST_BASE_URL } from '@/utils/constants';
5
+
6
+ export default async function GitHubGist({ id, figCaptionText }: GitHubGistType) {
7
+ try {
8
+ const headers: HeadersInit = {
9
+ 'Accept': 'application/vnd.github.v3+json',
10
+ };
11
+
12
+ if (process.env.GITHUB_TOKEN) {
13
+ headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
14
+ }
15
+
16
+ const response = await fetch(`https://api.github.com/gists/${id}`, {
17
+ headers,
18
+ next: { revalidate: 3600 },
19
+ });
20
+
21
+ if (!response.ok) {
22
+ return (
23
+ <div className="text-red-400 bg-red-950/30 border border-red-500/30 p-4 rounded-lg font-mono text-sm">
24
+ Could not load GitHub Gist ({response.status})
25
+ </div>
26
+ );
27
+ }
28
+
29
+ const data = await response.json();
30
+ const firstFileKey = Object.keys(data.files)[0];
31
+ const firstFile = data.files[firstFileKey];
32
+
33
+ const rawResponse = await fetch(firstFile.raw_url, {
34
+ headers: process.env.GITHUB_TOKEN
35
+ ? { 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}` }
36
+ : {},
37
+ next: { revalidate: 3600 },
38
+ });
39
+
40
+ const content = await rawResponse.text();
41
+ const language: string | null = firstFile.language;
42
+ const prismLanguage = language
43
+ ? (GITHUB_GIST_LANGUAGE_MAP[language] ?? language.toLowerCase())
44
+ : 'text';
45
+ const mdxGHGistURL = `${GIST_BASE_URL}/${GITHUB_USERNAME}/${id}`;
46
+
47
+ return (
48
+ <figure className="my-6">
49
+ <div className="relative bg-gray-900/30 rounded-lg border-2 border-green-500 shadow-[0_0_15px_rgba(34,197,94,0.3)] overflow-hidden">
50
+ {/* Header */}
51
+ <div className="flex flex-wrap items-center justify-between gap-2 px-4 py-2 bg-gray-800/50 border-b border-green-500/30">
52
+ <div className="flex items-center gap-2">
53
+ <svg className="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20">
54
+ <path fillRule="evenodd" d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" clipRule="evenodd" />
55
+ </svg>
56
+ <span className="text-sm text-green-200 font-medium">GitHub Gist</span>
57
+ {language && (
58
+ <span className="text-xs text-green-600 font-mono border border-green-500/20 px-1.5 py-0.5 rounded">
59
+ {language}
60
+ </span>
61
+ )}
62
+ </div>
63
+ <GistCopyButton content={content} mdxGHGistURL={mdxGHGistURL} />
64
+ </div>
65
+
66
+ {/* Code */}
67
+ <div className="scrollbar-gist overflow-auto max-h-96">
68
+ <GistCodeBlock content={content} language={prismLanguage} />
69
+ </div>
70
+ </div>
71
+ <figcaption className="mt-3 mb-4 leading-relaxed text-green-200/90 text-sm font-medium text-center">
72
+ {figCaptionText}
73
+ </figcaption>
74
+ </figure>
75
+ );
76
+ }
77
+ catch (err) {
78
+ return (
79
+ <div className="text-red-400 bg-red-950/30 border border-red-500/30 p-4 rounded-lg font-mono text-sm">
80
+ Could not load GitHub Gist: {err instanceof Error ? err.message : String(err)}
81
+ </div>
82
+ );
83
+ }
84
+ }
@@ -1,21 +1,23 @@
1
- import Image from "next/image";
2
- import type { MDXImageType } from "@/utils/types";
3
-
4
- // MDXImage custom component
5
- // Utilizes the built-in Next.js Image component as well as the figcaption element
6
- export default function MDXImage(imageProperties: MDXImageType) {
7
- return (
8
- <figure className='text-center'>
9
- <Image
10
- className='mx-auto'
11
- src={imageProperties.src}
12
- height={imageProperties.height}
13
- width={imageProperties.width}
14
- alt={imageProperties.alt}
15
- />
16
- <figcaption className="mb-4 leading-relaxed text-green-200/90">
17
- {imageProperties.figcaption}
18
- </figcaption>
19
- </figure>
20
- )
21
- }
1
+ import Image from "next/image";
2
+ import type { MDXImageType } from "@/utils/types";
3
+
4
+ // MDXImage custom component
5
+ // Utilizes the built-in Next.js Image component as well as the figcaption element
6
+ export default function MDXImage(imageProperties: MDXImageType) {
7
+ return (
8
+ <figure className='text-center my-6'>
9
+ <div className="relative inline-block p-4 bg-gray-900/30 rounded-lg border-2 border-green-500 shadow-[0_0_15px_rgba(34,197,94,0.3)] transition-all duration-300 hover:shadow-[0_0_25px_rgba(34,197,94,0.5)] hover:scale-[1.02]">
10
+ <Image
11
+ className='mx-auto rounded-md transition-transform duration-300'
12
+ src={imageProperties.src}
13
+ height={imageProperties.height}
14
+ width={imageProperties.width}
15
+ alt={imageProperties.alt}
16
+ />
17
+ </div>
18
+ <figcaption className="mt-3 mb-4 leading-relaxed text-green-200/90 text-sm font-medium">
19
+ {imageProperties.figcaption}
20
+ </figcaption>
21
+ </figure>
22
+ );
23
+ }
@@ -92,7 +92,7 @@
92
92
 
93
93
  > "The Virtual DOM is useful not because it's faster, but because it allows React to do declarative programming while maintaining good performance." - Dan Abramov, React core team
94
94
 
95
- ![React Virtual DOM visualization](https://mdx-blog-bucket.s3.us-east-2.amazonaws.com/blog-article-image.PNG)
95
+ <MDXImage src="https://mdx-blog-bucket.s3.us-east-2.amazonaws.com/blog-article-image.PNG" height="50" width="50" alt="No Image Found" figcaption="This is an image" />
96
96
 
97
97
  ## When to Worry About Re-renders
98
98
 
@@ -104,5 +104,4 @@
104
104
 
105
105
  ## Conclusion
106
106
 
107
- The Virtual DOM is a powerful abstraction that allows for a simpler mental model when building UI components. It enables React's declarative programming style while maintaining good performance through efficient updates to the DOM.
108
- `
107
+ The Virtual DOM is a powerful abstraction that allows for a simpler mental model when building UI components. It enables React's declarative programming style while maintaining good performance through efficient updates to the DOM.
@@ -0,0 +1,77 @@
1
+ export const GITHUB_USERNAME = process.env.GITHUB_USERNAME ?? "CodingAbdullah";
2
+ export const GIST_BASE_URL = process.env.GIST_BASE_URL ?? "https://mdxgists.net";
3
+
4
+ export const GITHUB_GIST_LANGUAGE_MAP: Record<string, string> = {
5
+ 'Assembly': 'asm',
6
+ 'Bash': 'bash',
7
+ 'C': 'c',
8
+ 'C#': 'csharp',
9
+ 'C++': 'cpp',
10
+ 'Cairo': 'cairo',
11
+ 'Clojure': 'clojure',
12
+ 'Crystal': 'crystal',
13
+ 'CSS': 'css',
14
+ 'Dart': 'dart',
15
+ 'Diff': 'diff',
16
+ 'Dockerfile': 'dockerfile',
17
+ 'Elixir': 'elixir',
18
+ 'Erlang': 'erlang',
19
+ 'F#': 'fsharp',
20
+ 'GLSL': 'glsl',
21
+ 'Go': 'go',
22
+ 'GraphQL': 'graphql',
23
+ 'GraphQL Schema': 'graphql',
24
+ 'Groovy': 'groovy',
25
+ 'Haml': 'haml',
26
+ 'Handlebars': 'handlebars',
27
+ 'Haskell': 'haskell',
28
+ 'HCL': 'hcl',
29
+ 'HTML': 'html',
30
+ 'INI': 'ini',
31
+ 'Java': 'java',
32
+ 'JavaScript': 'javascript',
33
+ 'JSON': 'json',
34
+ 'JSX': 'jsx',
35
+ 'Julia': 'julia',
36
+ 'Kotlin': 'kotlin',
37
+ 'Less': 'less',
38
+ 'Liquid': 'liquid',
39
+ 'Lua': 'lua',
40
+ 'Makefile': 'makefile',
41
+ 'Markdown': 'markdown',
42
+ 'MATLAB': 'matlab',
43
+ 'Maven POM': 'xml',
44
+ 'MDX': 'mdx',
45
+ 'Nginx': 'nginx',
46
+ 'Nim': 'nim',
47
+ 'OCaml': 'ocaml',
48
+ 'Perl': 'perl',
49
+ 'PHP': 'php',
50
+ 'PowerShell': 'powershell',
51
+ 'Protocol Buffer': 'protobuf',
52
+ 'Pug': 'pug',
53
+ 'Python': 'python',
54
+ 'R': 'r',
55
+ 'Ruby': 'ruby',
56
+ 'Rust': 'rust',
57
+ 'Sass': 'sass',
58
+ 'Scala': 'scala',
59
+ 'SCSS': 'scss',
60
+ 'Shell': 'bash',
61
+ 'Solidity': 'solidity',
62
+ 'SPARQL': 'sparql',
63
+ 'SQL': 'sql',
64
+ 'Svelte': 'svelte',
65
+ 'Swift': 'swift',
66
+ 'Terraform': 'terraform',
67
+ 'TOML': 'toml',
68
+ 'TSX': 'tsx',
69
+ 'TypeScript': 'typescript',
70
+ 'V': 'v',
71
+ 'Vue': 'vue',
72
+ 'Vyper': 'vyper',
73
+ 'WebAssembly': 'wasm',
74
+ 'XML': 'xml',
75
+ 'YAML': 'yaml',
76
+ 'Zig': 'zig',
77
+ };
@@ -8,3 +8,4 @@ export {
8
8
  JS_EXAMPLES,
9
9
  TS_EXAMPLES,
10
10
  } from "./SandboxExamples";
11
+ export { GITHUB_USERNAME, GITHUB_GIST_LANGUAGE_MAP, GIST_BASE_URL } from "./GitHubGistConstants";
package/tsconfig.json CHANGED
@@ -36,6 +36,7 @@
36
36
  ],
37
37
  "include": [
38
38
  "next-env.d.ts",
39
+ "mdx.d.ts",
39
40
  "**/*.mdx",
40
41
  "**/*.ts",
41
42
  "**/*.tsx",
@@ -45,4 +46,4 @@
45
46
  "exclude": [
46
47
  "node_modules"
47
48
  ]
48
- }
49
+ }