├── .eslintrc.json ├── postcss.config.mjs ├── src ├── app │ ├── providers.tsx │ ├── client-layout.tsx │ ├── globals.css │ ├── layout.tsx │ ├── blog │ │ ├── page.tsx │ │ └── [slug] │ │ │ └── page.tsx │ └── page.tsx ├── lib │ ├── dateFormatter.ts │ ├── config.ts │ ├── utils.ts │ ├── markdown.ts │ └── getBlogPosts.ts ├── utils │ └── dateFormatter.ts ├── components │ ├── ProgressBar.tsx │ ├── ClientSideImage.tsx │ ├── ErrorBoundary.tsx │ ├── ScrollProgressBar.tsx │ ├── ScrollDepthTracker.tsx │ ├── ScrollToTopButton.tsx │ ├── Subscribe.tsx │ ├── RelatedPosts.tsx │ ├── SocialShare.tsx │ ├── BlogList.tsx │ └── BlogPost.tsx └── types │ └── index.ts ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── .gitignore ├── LICENSE ├── package.json ├── next.config.mjs ├── tailwind.config.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | 9 | export default config; -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | // src/app/providers.tsx 2 | 'use client'; 3 | 4 | import { ReactNode } from 'react'; 5 | 6 | interface ProvidersProps { 7 | children: ReactNode; 8 | } 9 | 10 | export default function Providers({ children }: ProvidersProps) { 11 | return <>{children}; 12 | } -------------------------------------------------------------------------------- /src/lib/dateFormatter.ts: -------------------------------------------------------------------------------- 1 | // src/lib/dateFormatter.ts 2 | export function formatDate(dateString: string): string { 3 | const date = new Date(dateString); 4 | return new Intl.DateTimeFormat('en-US', { 5 | year: 'numeric', 6 | month: 'long', 7 | day: 'numeric', 8 | }).format(date); 9 | } -------------------------------------------------------------------------------- /src/app/client-layout.tsx: -------------------------------------------------------------------------------- 1 | // src/app/client-layout.tsx 2 | 'use client' 3 | 4 | import Providers from './providers' 5 | import '@fontsource/inter' 6 | 7 | export default function ClientLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode 11 | }) { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } -------------------------------------------------------------------------------- /src/utils/dateFormatter.ts: -------------------------------------------------------------------------------- 1 | // src/utils/dateFormatter.ts 2 | 3 | export function formatDate(dateString: string): string { 4 | const date = new Date(dateString); 5 | const options: Intl.DateTimeFormatOptions = { 6 | year: 'numeric', 7 | month: 'long', 8 | day: 'numeric' 9 | }; 10 | return date.toLocaleDateString('en-US', options); 11 | } -------------------------------------------------------------------------------- /src/components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ProgressBar.tsx 2 | import React from 'react'; 3 | 4 | interface ProgressBarProps { 5 | progress: number; 6 | } 7 | 8 | const ProgressBar: React.FC = ({ progress }) => { 9 | return ( 10 |
11 |
15 |
16 | ); 17 | }; 18 | 19 | export default ProgressBar; -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: 'Next.js GitHub Markdown Blog', 3 | description: 'A modern blog platform powered by Next.js and GitHub Markdown', 4 | url: 'https://your-domain.com', 5 | ogImage: 'https://your-domain.com/og.jpg', 6 | links: { 7 | github: 'https://github.com/yourusername/your-repo', 8 | twitter: 'https://twitter.com/yourusername', 9 | }, 10 | postsPerPage: 10, 11 | defaultAuthor: { 12 | name: 'Anonymous', 13 | image: '/images/default-avatar.jpg', 14 | bio: 'A mysterious writer', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // src/lib/utils.ts 2 | import readingTime from 'reading-time'; 3 | 4 | export function getReadingTime(content: string): string { 5 | const result = readingTime(content); 6 | return result.text; 7 | } 8 | 9 | export function slugify(text: string): string { 10 | return text 11 | .toLowerCase() 12 | .replace(/[^\w\s-]/g, '') 13 | .replace(/[\s_-]+/g, '-') 14 | .replace(/^-+|-+$/g, ''); 15 | } 16 | 17 | export function truncate(text: string, length: number): string { 18 | if (text.length <= length) return text; 19 | return text.slice(0, length).trim() + '...'; 20 | } 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // src/types/index.ts 2 | 3 | export interface BlogPostProps { 4 | slug: string; 5 | title: string; 6 | date: string; 7 | content: string; 8 | htmlContent: string; 9 | excerpt: string; 10 | category: string; 11 | tags: string[]; 12 | coverImage: string; 13 | author: string; 14 | authorImage: string; 15 | authorBio: string; 16 | readingTime: string; 17 | } 18 | 19 | export interface BlogMetadata { 20 | title: string; 21 | date: string; 22 | excerpt: string; 23 | category: string; 24 | tags: string[]; 25 | coverImage: string; 26 | author: string; 27 | authorImage: string; 28 | authorBio: string; 29 | } -------------------------------------------------------------------------------- /src/components/ClientSideImage.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ClientSideImage.tsx 2 | 'use client'; 3 | 4 | import React from 'react'; 5 | import Image, { ImageProps } from 'next/image'; 6 | 7 | interface ClientSideImageProps extends ImageProps { 8 | alt: string; // Make alt prop required 9 | } 10 | 11 | const ClientSideImage: React.FC = (props) => { 12 | const { onError, alt, ...rest } = props; 13 | 14 | return ( 15 | {alt} { 19 | if (onError) { 20 | onError(e); 21 | } else { 22 | e.currentTarget.src = '/images/placeholder.jpg'; 23 | console.error('Failed to load image:', props.src); 24 | } 25 | }} 26 | /> 27 | ); 28 | }; 29 | 30 | export default ClientSideImage; -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | 2 | @import 'tailwindcss/base'; 3 | @import 'tailwindcss/components'; 4 | @import 'tailwindcss/utilities'; 5 | 6 | @layer base { 7 | :root { 8 | --background: 0 0% 100%; 9 | --foreground: 222.2 84% 4.9%; 10 | --primary: 221.2 83.2% 53.3%; 11 | --primary-foreground: 210 40% 98%; 12 | } 13 | 14 | .dark { 15 | --background: 222.2 84% 4.9%; 16 | --foreground: 210 40% 98%; 17 | --primary: 217.2 91.2% 59.8%; 18 | --primary-foreground: 222.2 47.4% 11.2%; 19 | } 20 | } 21 | 22 | .prose { 23 | max-width: 65ch; 24 | @apply mx-auto; 25 | } 26 | 27 | .prose img { 28 | @apply rounded-lg shadow-lg; 29 | } 30 | 31 | .prose a { 32 | @apply text-blue-600 hover:text-blue-800 transition-colors; 33 | } 34 | 35 | .prose h1, .prose h2, .prose h3, .prose h4 { 36 | @apply scroll-mt-20; 37 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "baseUrl": ".", 13 | "target": "es2015", 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | }, 30 | "esModuleInterop": true 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ErrorBoundary.tsx 2 | 'use client'; 3 | 4 | import { Component, ErrorInfo, ReactNode } from 'react'; 5 | 6 | interface Props { 7 | children?: ReactNode; 8 | fallback: ReactNode; 9 | } 10 | 11 | interface State { 12 | hasError: boolean; 13 | } 14 | 15 | class ErrorBoundary extends Component { 16 | public state: State = { 17 | hasError: false 18 | }; 19 | 20 | public static getDerivedStateFromError(_: Error): State { 21 | return { hasError: true }; 22 | } 23 | 24 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) { 25 | console.error('Uncaught error:', error, errorInfo); 26 | } 27 | 28 | public render() { 29 | if (this.state.hasError) { 30 | return this.props.fallback; 31 | } 32 | 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | node_modules 38 | .next 39 | ./package-lock.json 40 | package-lock.json 41 | hn_single_file.txt 42 | bulk_transcript_optimizer_single_file__nextjs_frontend.txt 43 | package-lock.json 44 | bun.lockb 45 | /bun.lockb 46 | .env 47 | .env*.local 48 | node_modules 49 | .next 50 | out 51 | build 52 | dist 53 | .DS_Store 54 | *.pem 55 | npm-debug.log* 56 | yarn-debug.log* 57 | yarn-error.log* 58 | .vercel 59 | -------------------------------------------------------------------------------- /src/components/ScrollProgressBar.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ScrollProgressBar.tsx 2 | 'use client'; 3 | 4 | import React, { useEffect, useState } from 'react'; 5 | 6 | const ScrollProgressBar: React.FC = () => { 7 | const [scrollProgress, setScrollProgress] = useState(0); 8 | 9 | useEffect(() => { 10 | const handleScroll = () => { 11 | const totalScroll = document.documentElement.scrollHeight - document.documentElement.clientHeight; 12 | const currentScroll = window.scrollY; 13 | const scrollPercentage = (currentScroll / totalScroll) * 100; 14 | setScrollProgress(scrollPercentage); 15 | }; 16 | 17 | window.addEventListener('scroll', handleScroll); 18 | return () => window.removeEventListener('scroll', handleScroll); 19 | }, []); 20 | 21 | return ( 22 |
26 | ); 27 | }; 28 | 29 | export default ScrollProgressBar; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jeff Emanuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/ScrollDepthTracker.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ScrollDepthTracker.tsx 2 | 'use client'; 3 | 4 | import React, { useEffect, useState } from 'react'; 5 | 6 | const ScrollDepthTracker: React.FC = () => { 7 | const [scrollPercentages, setScrollPercentages] = useState([]); 8 | 9 | useEffect(() => { 10 | const handleScroll = () => { 11 | const scrollPosition = window.scrollY; 12 | const totalHeight = document.documentElement.scrollHeight - window.innerHeight; 13 | const scrollPercentage = Math.round((scrollPosition / totalHeight) * 100); 14 | 15 | const thresholds = [25, 50, 75, 100]; 16 | thresholds.forEach(threshold => { 17 | if (scrollPercentage >= threshold && !scrollPercentages.includes(threshold)) { 18 | setScrollPercentages(prev => [...prev, threshold]); 19 | // You can add your own analytics tracking here 20 | console.log(`Reached ${threshold}% scroll depth`); 21 | } 22 | }); 23 | }; 24 | 25 | window.addEventListener('scroll', handleScroll); 26 | return () => window.removeEventListener('scroll', handleScroll); 27 | }, [scrollPercentages]); 28 | 29 | return null; 30 | }; 31 | 32 | export default ScrollDepthTracker; -------------------------------------------------------------------------------- /src/components/ScrollToTopButton.tsx: -------------------------------------------------------------------------------- 1 | 2 | // src/components/ScrollToTopButton.tsx 3 | import React, { useState, useEffect } from 'react'; 4 | import { FaArrowUp } from 'react-icons/fa'; 5 | 6 | const ScrollToTopButton: React.FC = () => { 7 | const [isVisible, setIsVisible] = useState(false); 8 | 9 | useEffect(() => { 10 | const toggleVisibility = () => { 11 | if (window.pageYOffset > 300) { 12 | setIsVisible(true); 13 | } else { 14 | setIsVisible(false); 15 | } 16 | }; 17 | 18 | window.addEventListener('scroll', toggleVisibility); 19 | return () => window.removeEventListener('scroll', toggleVisibility); 20 | }, []); 21 | 22 | const scrollToTop = () => { 23 | window.scrollTo({ 24 | top: 0, 25 | behavior: 'smooth', 26 | }); 27 | }; 28 | 29 | return ( 30 | <> 31 | {isVisible && ( 32 | 39 | )} 40 | 41 | ); 42 | }; 43 | 44 | export default ScrollToTopButton; -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-github-markdown-blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@fontsource/inter": "^5.1.0", 13 | "framer-motion": "^11.11.10", 14 | "gray-matter": "^4.0.3", 15 | "next": "^14.2.16", 16 | "react": "18.3.1", 17 | "react-dom": "^18.3.1", 18 | "react-icons": "^5.3.0", 19 | "reading-time": "^1.5.0", 20 | "rehype": "^13.0.2", 21 | "rehype-highlight": "^7.0.1", 22 | "rehype-prism-plus": "2.0.0", 23 | "rehype-raw": "^7.0.0", 24 | "rehype-sanitize": "6.0.0", 25 | "rehype-stringify": "10.0.1", 26 | "remark": "15.0.1", 27 | "remark-gfm": "4.0.0", 28 | "remark-html": "^16.0.1", 29 | "remark-parse": "11.0.0", 30 | "remark-rehype": "11.1.1", 31 | "sharp": "^0.33.5", 32 | "unified": "11.0.5" 33 | }, 34 | "devDependencies": { 35 | "@tailwindcss/typography": "^0.5.15", 36 | "@types/node": "^22.8.4", 37 | "@types/react": "^18.3.12", 38 | "@types/react-dom": "^18.3.1", 39 | "autoprefixer": "^10.4.20", 40 | "eslint": "^8.57.1", 41 | "eslint-config-next": "14.2.5", 42 | "postcss": "^8.4.47", 43 | "tailwindcss": "^3.4.14", 44 | "typescript": "^5.6.3" 45 | } 46 | } -------------------------------------------------------------------------------- /src/components/Subscribe.tsx: -------------------------------------------------------------------------------- 1 | // src/components/Subscribe.tsx 2 | 'use client'; 3 | 4 | import React, { useState } from 'react'; 5 | 6 | const Subscribe: React.FC = () => { 7 | const [email, setEmail] = useState(''); 8 | 9 | const handleSubmit = (e: React.FormEvent) => { 10 | e.preventDefault(); 11 | // Handle subscription logic here 12 | console.log('Subscribing email:', email); 13 | // Reset form 14 | setEmail(''); 15 | }; 16 | 17 | return ( 18 |
19 |

Subscribe to Our Newsletter

20 |

Stay up to date with our latest posts and updates!

21 |
22 | setEmail(e.target.value)} 26 | placeholder="Enter your email" 27 | required 28 | className="flex-grow px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" 29 | /> 30 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Subscribe; -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "raw.githubusercontent.com", 8 | pathname: "/**", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "github.com", 13 | pathname: "/**", 14 | }, 15 | { 16 | protocol: "https", 17 | hostname: "**.githubusercontent.com", 18 | pathname: "/**", 19 | }, 20 | { 21 | protocol: "https", 22 | hostname: "pbs.twimg.com", 23 | pathname: "/**", 24 | }, 25 | { 26 | protocol: "https", 27 | hostname: "www.youtube.com", 28 | pathname: "/**", 29 | }, 30 | ], 31 | }, 32 | async headers() { 33 | return [ 34 | { 35 | source: '/(.*)', 36 | headers: [ 37 | { 38 | key: 'X-Frame-Options', 39 | value: 'DENY', 40 | }, 41 | { 42 | key: 'X-Content-Type-Options', 43 | value: 'nosniff', 44 | }, 45 | { 46 | key: 'Referrer-Policy', 47 | value: 'strict-origin-when-cross-origin', 48 | }, 49 | { 50 | key: 'Permissions-Policy', 51 | value: 'camera=(), microphone=(), geolocation=()', 52 | }, 53 | ], 54 | } 55 | ]; 56 | }, 57 | poweredByHeader: false, 58 | }; 59 | 60 | export default nextConfig; 61 | -------------------------------------------------------------------------------- /src/components/RelatedPosts.tsx: -------------------------------------------------------------------------------- 1 | // src/components/RelatedPosts.tsx 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import ClientSideImage from './ClientSideImage'; 5 | import { BlogPostProps } from '@/lib/getBlogPosts'; 6 | import ErrorBoundary from './ErrorBoundary'; 7 | 8 | const RelatedPosts: React.FC<{ posts: BlogPostProps[] }> = ({ posts }) => { 9 | return ( 10 | Error loading related posts}> 11 |
12 |

Related Posts

13 |
14 | {posts.map((post) => ( 15 | 16 |
17 | 24 |
25 |

{post.title}

26 |

{post.excerpt}

27 |
28 |
29 | 30 | ))} 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default RelatedPosts; 38 | 39 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | // src/app/layout.tsx 2 | import './globals.css'; 3 | import { Inter } from 'next/font/google'; 4 | import type { Metadata } from 'next'; 5 | 6 | const inter = Inter({ subsets: ['latin'] }); 7 | 8 | export const metadata: Metadata = { 9 | metadataBase: new URL('https://your-domain.com'), // Replace with your domain 10 | title: { 11 | default: 'Next.js GitHub Markdown Blog', 12 | template: '%s | Next.js GitHub Markdown Blog', 13 | }, 14 | description: 'A modern blog platform powered by Next.js and GitHub Markdown', 15 | openGraph: { 16 | title: 'Next.js GitHub Markdown Blog', 17 | description: 'A modern blog platform powered by Next.js and GitHub Markdown', 18 | url: '/', 19 | siteName: 'Next.js GitHub Markdown Blog', 20 | images: [ 21 | { 22 | url: '/images/og-image.jpg', // Add your own OG image 23 | width: 1200, 24 | height: 630, 25 | alt: 'Next.js GitHub Markdown Blog', 26 | }, 27 | ], 28 | locale: 'en_US', 29 | type: 'website', 30 | }, 31 | twitter: { 32 | card: 'summary_large_image', 33 | title: 'Next.js GitHub Markdown Blog', 34 | description: 'A modern blog platform powered by Next.js and GitHub Markdown', 35 | images: ['/images/twitter-image.jpg'], // Add your own Twitter card image 36 | }, 37 | }; 38 | 39 | export default function RootLayout({ 40 | children, 41 | }: { 42 | children: React.ReactNode; 43 | }) { 44 | return ( 45 | 46 | 47 | {children} 48 | 49 | 50 | ); 51 | } 52 | 53 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | typography: { 12 | DEFAULT: { 13 | css: { 14 | maxWidth: '65ch', 15 | color: 'inherit', 16 | a: { 17 | color: '#3182ce', 18 | '&:hover': { 19 | color: '#2c5282', 20 | }, 21 | }, 22 | 'h1,h2,h3,h4': { 23 | color: 'inherit', 24 | fontWeight: '700', 25 | }, 26 | code: { 27 | color: '#805ad5', 28 | }, 29 | 'code::before': { 30 | content: '""', 31 | }, 32 | 'code::after': { 33 | content: '""', 34 | }, 35 | 'blockquote p:first-of-type::before': { content: 'none' }, 36 | 'blockquote p:last-of-type::after': { content: 'none' }, 37 | }, 38 | }, 39 | }, 40 | colors: { 41 | primary: { 42 | 50: '#f0f9ff', 43 | 100: '#e0f2fe', 44 | 200: '#bae6fd', 45 | 300: '#7dd3fc', 46 | 400: '#38bdf8', 47 | 500: '#0ea5e9', 48 | 600: '#0284c7', 49 | 700: '#0369a1', 50 | 800: '#075985', 51 | 900: '#0c4a6e', 52 | }, 53 | }, 54 | fontFamily: { 55 | sans: ['Inter var', 'sans-serif'], 56 | }, 57 | }, 58 | }, 59 | plugins: [ 60 | require('@tailwindcss/typography'), 61 | ], 62 | }; 63 | 64 | export default config; -------------------------------------------------------------------------------- /src/components/SocialShare.tsx: -------------------------------------------------------------------------------- 1 | // src/components/SocialShare.tsx 2 | 'use client'; 3 | 4 | import React, { useEffect, useState } from 'react'; 5 | import { FaTwitter, FaFacebook, FaLinkedin } from 'react-icons/fa'; 6 | import { IconType } from 'react-icons'; 7 | 8 | interface SocialShareProps { 9 | title: string; 10 | } 11 | 12 | const SocialShare: React.FC = ({ title }) => { 13 | const [currentUrl, setCurrentUrl] = useState(''); 14 | 15 | useEffect(() => { 16 | setCurrentUrl(window.location.href); 17 | }, []); 18 | 19 | const socialPlatforms: { 20 | name: string; 21 | icon: IconType; 22 | getShareUrl: (url: string, title: string) => string; 23 | }[] = [ 24 | { 25 | name: 'twitter', 26 | icon: FaTwitter, 27 | getShareUrl: (url: string, title: string) => 28 | `https://twitter.com/share?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`, 29 | }, 30 | { 31 | name: 'facebook', 32 | icon: FaFacebook, 33 | getShareUrl: (url: string) => 34 | `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, 35 | }, 36 | { 37 | name: 'linkedin', 38 | icon: FaLinkedin, 39 | getShareUrl: (url: string, title: string) => 40 | `https://www.linkedin.com/shareArticle?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`, 41 | }, 42 | ]; 43 | 44 | return ( 45 |
46 | {socialPlatforms.map(({ name, icon: Icon, getShareUrl }) => ( 47 | 54 | 55 | 56 | ))} 57 |
58 | ); 59 | }; 60 | 61 | export default SocialShare; -------------------------------------------------------------------------------- /src/app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | // src/app/blog/page.tsx 2 | import React from 'react'; 3 | import { getBlogPosts } from '@/lib/getBlogPosts'; 4 | import dynamic from 'next/dynamic'; 5 | import { Metadata } from 'next'; 6 | 7 | // Dynamically import client components 8 | const BlogList = dynamic(() => import('@/components/BlogList'), { 9 | ssr: true 10 | }); 11 | 12 | const ErrorBoundary = dynamic(() => import('@/components/ErrorBoundary'), { 13 | ssr: false 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Blog', 18 | description: 'Read our latest blog posts', 19 | }; 20 | 21 | export default async function BlogPage() { 22 | try { 23 | const posts = await getBlogPosts(); 24 | 25 | return ( 26 | An error occurred while loading the blog posts. Please try again later.}> 27 |
28 |
33 |
34 |

35 | Blog 36 |

37 |

38 | Thoughts, stories and ideas 39 |

40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 | ); 51 | } catch (error) { 52 | console.error('Error in BlogPage:', error); 53 | return
An error occurred while loading the blog page. Please try again later.
; 54 | } 55 | } -------------------------------------------------------------------------------- /src/lib/markdown.ts: -------------------------------------------------------------------------------- 1 | // src/lib/markdown.ts 2 | import { remark } from 'remark'; 3 | import remarkParse from 'remark-parse'; 4 | import remarkGfm from 'remark-gfm'; 5 | import remarkRehype from 'remark-rehype'; 6 | import rehypePrism from 'rehype-prism-plus'; 7 | import rehypeSanitize from 'rehype-sanitize'; 8 | import rehypeStringify from 'rehype-stringify'; 9 | import { visit } from 'unist-util-visit'; 10 | import { Element, Root } from 'hast'; 11 | import { Plugin } from 'unified'; 12 | 13 | const customRenderer: Plugin<[], Root> = () => { 14 | return (tree: Root) => { 15 | visit(tree, 'element', (node: Element) => { 16 | if (node.tagName) { 17 | const props = node.properties || {}; 18 | 19 | switch (node.tagName) { 20 | case 'h1': 21 | props.className = [ 22 | 'text-4xl', 23 | 'font-bold', 24 | 'text-gray-900', 25 | 'mt-8', 26 | 'mb-4', 27 | 'scroll-mt-20', 28 | ]; 29 | break; 30 | case 'h2': 31 | props.className = [ 32 | 'text-3xl', 33 | 'font-bold', 34 | 'text-gray-900', 35 | 'mt-6', 36 | 'mb-4', 37 | 'scroll-mt-20', 38 | ]; 39 | break; 40 | case 'h3': 41 | props.className = [ 42 | 'text-2xl', 43 | 'font-bold', 44 | 'text-gray-900', 45 | 'mt-4', 46 | 'mb-2', 47 | 'scroll-mt-20', 48 | ]; 49 | break; 50 | case 'p': 51 | props.className = [ 52 | 'text-gray-800', 53 | 'leading-relaxed', 54 | 'mb-4', 55 | ]; 56 | break; 57 | case 'a': 58 | props.className = [ 59 | 'text-blue-600', 60 | 'hover:text-blue-800', 61 | 'transition-colors', 62 | 'duration-200', 63 | ]; 64 | props.target = '_blank'; 65 | props.rel = 'noopener noreferrer'; 66 | break; 67 | case 'ul': 68 | props.className = ['list-disc', 'list-inside', 'mb-4', 'space-y-2']; 69 | break; 70 | case 'ol': 71 | props.className = ['list-decimal', 'list-inside', 'mb-4', 'space-y-2']; 72 | break; 73 | case 'blockquote': 74 | props.className = [ 75 | 'border-l-4', 76 | 'border-gray-200', 77 | 'pl-4', 78 | 'italic', 79 | 'my-4', 80 | 'text-gray-600', 81 | ]; 82 | break; 83 | case 'code': 84 | props.className = props.className || []; 85 | if (typeof props.className === 'object') { 86 | props.className.push('language-javascript'); 87 | } 88 | break; 89 | case 'pre': 90 | props.className = [ 91 | 'bg-gray-900', 92 | 'rounded-lg', 93 | 'p-4', 94 | 'overflow-x-auto', 95 | 'mb-4', 96 | ]; 97 | break; 98 | case 'img': 99 | props.className = ['rounded-lg', 'shadow-lg', 'my-4']; 100 | break; 101 | default: 102 | break; 103 | } 104 | 105 | node.properties = props; 106 | } 107 | }); 108 | }; 109 | }; 110 | 111 | export async function markdownToHtml(markdown: string) { 112 | const result = await remark() 113 | .use(remarkParse) 114 | .use(remarkGfm) 115 | .use(remarkRehype) 116 | .use(customRenderer) 117 | .use(rehypePrism) 118 | .use(rehypeSanitize) 119 | .use(rehypeStringify) 120 | .process(markdown); 121 | 122 | return result.toString(); 123 | } -------------------------------------------------------------------------------- /src/lib/getBlogPosts.ts: -------------------------------------------------------------------------------- 1 | // src/lib/getBlogPosts.ts 2 | import matter from 'gray-matter'; 3 | import { markdownToHtml } from './markdown'; 4 | import readingTime from 'reading-time'; 5 | 6 | export interface BlogPostProps { 7 | slug: string; 8 | title: string; 9 | date: string; 10 | content: string; 11 | htmlContent: string; 12 | excerpt: string; 13 | category: string; 14 | tags: string[]; 15 | coverImage: string; 16 | author: string; 17 | authorImage: string; 18 | authorBio: string; 19 | readingTime: string; 20 | } 21 | 22 | // Get these from environment variables 23 | const GITHUB_REPO = process.env.GITHUB_REPO || 'username/repo'; 24 | const GITHUB_API = `https://api.github.com/repos/${GITHUB_REPO}/contents`; 25 | const GITHUB_RAW = `https://raw.githubusercontent.com/${GITHUB_REPO}/main`; 26 | 27 | export async function getBlogPosts(): Promise { 28 | console.log('Fetching blog posts from GitHub...'); 29 | 30 | try { 31 | const headers: HeadersInit = { 32 | 'Accept': 'application/vnd.github.v3+json', 33 | 'User-Agent': 'NextJS-Blog', 34 | 'Cache-Control': 'no-cache', 35 | 'If-None-Match': '', // Bypass ETag caching 36 | }; 37 | 38 | // Add GitHub token if available 39 | if (process.env.GITHUB_TOKEN) { 40 | headers.Authorization = `token ${process.env.GITHUB_TOKEN}`; 41 | } 42 | 43 | const response = await fetch(GITHUB_API, { headers }); 44 | 45 | if (!response.ok) { 46 | throw new Error(`GitHub API responded with status ${response.status}: ${await response.text()}`); 47 | } 48 | 49 | const files: any[] = await response.json(); 50 | const mdFiles = files.filter(file => file.name.endsWith('.md')); 51 | 52 | console.log(`Found ${mdFiles.length} Markdown files`); 53 | 54 | const posts = await Promise.all(mdFiles.map(async (file) => { 55 | try { 56 | const contentResponse = await fetch(`${GITHUB_RAW}/${file.path}`, { headers }); 57 | 58 | if (!contentResponse.ok) { 59 | throw new Error(`Failed to fetch content for ${file.name}: ${contentResponse.status}`); 60 | } 61 | 62 | const content = await contentResponse.text(); 63 | const { data, content: markdown } = matter(content); 64 | 65 | // Validate required fields 66 | if (!data.title || !data.date) { 67 | console.warn(`Warning: Required fields missing in ${file.name}`); 68 | data.title = data.title || 'Untitled'; 69 | data.date = data.date || new Date().toISOString(); 70 | } 71 | 72 | // Convert markdown to HTML 73 | const htmlContent = await markdownToHtml(markdown); 74 | 75 | const post: BlogPostProps = { 76 | slug: file.name.replace('.md', ''), 77 | title: data.title, 78 | date: new Date(data.date).toISOString(), 79 | content: markdown, 80 | htmlContent, 81 | excerpt: data.excerpt || '', 82 | category: data.category || 'Uncategorized', 83 | tags: data.tags || [], 84 | coverImage: data.coverImage || '/images/default-cover.jpg', 85 | author: data.author || 'Anonymous', 86 | authorImage: data.authorImage || '/images/default-avatar.jpg', 87 | authorBio: data.authorBio || '', 88 | readingTime: readingTime(markdown).text, 89 | }; 90 | 91 | return post; 92 | } catch (error) { 93 | console.error(`Error processing file ${file.name}:`, error); 94 | return null; 95 | } 96 | })); 97 | 98 | // Filter out any null posts and sort by date 99 | const validPosts = posts.filter((post): post is BlogPostProps => post !== null); 100 | const sortedPosts = validPosts.sort( 101 | (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() 102 | ); 103 | 104 | return sortedPosts; 105 | } catch (error) { 106 | console.error('Error in getBlogPosts:', error); 107 | throw error; 108 | } 109 | } -------------------------------------------------------------------------------- /src/app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | // src/app/blog/[slug]/page.tsx 2 | import { getBlogPosts, BlogPostProps } from '@/lib/getBlogPosts'; 3 | import dynamic from 'next/dynamic'; 4 | import { Metadata } from 'next'; 5 | import { notFound } from 'next/navigation'; 6 | import Link from 'next/link'; 7 | 8 | // Dynamically import client components 9 | const BlogPost = dynamic(() => import('@/components/BlogPost'), { 10 | ssr: true 11 | }); 12 | 13 | const ErrorBoundary = dynamic(() => import('@/components/ErrorBoundary'), { 14 | ssr: false 15 | }); 16 | 17 | export async function generateStaticParams() { 18 | try { 19 | const posts = await getBlogPosts(); 20 | return posts.map((post) => ({ 21 | slug: post.slug, 22 | })); 23 | } catch (error) { 24 | console.error('Error generating static params:', error); 25 | return []; 26 | } 27 | } 28 | 29 | export async function generateMetadata({ params }: { params: { slug: string } }): Promise { 30 | try { 31 | const posts = await getBlogPosts(); 32 | const post = posts.find((p) => p.slug === params.slug); 33 | 34 | if (!post) { 35 | return { 36 | title: 'Post Not Found', 37 | }; 38 | } 39 | 40 | return { 41 | title: post.title, 42 | description: post.excerpt, 43 | openGraph: { 44 | title: post.title, 45 | description: post.excerpt, 46 | images: post.coverImage ? [{ url: post.coverImage }] : undefined, 47 | }, 48 | twitter: { 49 | card: 'summary_large_image', 50 | title: post.title, 51 | description: post.excerpt, 52 | images: post.coverImage ? [post.coverImage] : undefined, 53 | }, 54 | }; 55 | } catch (error) { 56 | console.error('Error generating metadata:', error); 57 | return { 58 | title: 'Error', 59 | description: 'An error occurred while generating metadata', 60 | }; 61 | } 62 | } 63 | 64 | const getRelatedPosts = (currentPost: BlogPostProps, allPosts: BlogPostProps[], limit = 3): BlogPostProps[] => { 65 | return allPosts 66 | .filter(p => p.slug !== currentPost.slug) 67 | .sort((a, b) => { 68 | const aCommonTags = a.tags?.filter(tag => currentPost.tags?.includes(tag)).length ?? 0; 69 | const bCommonTags = b.tags?.filter(tag => currentPost.tags?.includes(tag)).length ?? 0; 70 | return bCommonTags - aCommonTags; 71 | }) 72 | .slice(0, limit); 73 | }; 74 | 75 | export default async function BlogPostPage({ params }: { params: { slug: string } }) { 76 | try { 77 | const posts = await getBlogPosts(); 78 | const post = posts.find((p) => p.slug === params.slug); 79 | 80 | if (!post) { 81 | notFound(); 82 | } 83 | 84 | const relatedPosts = getRelatedPosts(post, posts); 85 | 86 | return ( 87 | 89 |

Error Rendering Blog Post

90 |

We're sorry, but an error occurred while trying to render this blog post.

91 | 92 | Return to Blog 93 | 94 | 95 | }> 96 | 97 |
98 | ); 99 | } catch (error) { 100 | console.error('Error in BlogPostPage:', error); 101 | return ( 102 |
103 |

Error Loading Blog Post

104 |

We're sorry, but an error occurred while trying to load this blog post.

105 |

Error details: {error instanceof Error ? error.message : String(error)}

106 | 107 | Return to Blog 108 | 109 |
110 | ); 111 | } 112 | } -------------------------------------------------------------------------------- /src/components/BlogList.tsx: -------------------------------------------------------------------------------- 1 | // src/components/BlogList.tsx 2 | 'use client'; 3 | 4 | import React from 'react'; 5 | import Link from 'next/link'; 6 | import ClientSideImage from './ClientSideImage'; 7 | import { BlogPostProps } from '@/lib/getBlogPosts'; 8 | import ErrorBoundary from '@/components/ErrorBoundary'; 9 | 10 | const BlogList: React.FC<{ posts: BlogPostProps[] }> = ({ posts }) => { 11 | return ( 12 | Error loading blog posts}> 13 |
14 | {posts.length === 0 ? ( 15 |
No posts found
16 | ) : ( 17 | posts.map((post, index) => ( 18 | 19 | {index === 0 ? : } 20 | 21 | )) 22 | )} 23 |
24 |
25 | ); 26 | }; 27 | 28 | const FeaturedPost: React.FC<{ post: BlogPostProps }> = ({ post }) => ( 29 | Error loading featured post}> 30 |
31 | 32 |
33 | 40 |
41 |
42 |
43 |

{post.category.toUpperCase()}

44 |
{post.title}
45 |

{post.excerpt}

46 |
47 |
48 |
49 |
50 | 58 |

{post.author}

59 |
60 |

{post.readingTime}

61 |
62 |
63 |
64 | 65 |
66 |
67 | ); 68 | 69 | const RegularPost: React.FC<{ post: BlogPostProps }> = ({ post }) => ( 70 | Error loading post}> 71 |
72 |
73 | 74 | 81 |
82 |

{post.category.toUpperCase()}

83 |
{post.title}
84 |

{post.excerpt}

85 |
86 | 87 |
88 |
89 |
90 |
91 | 99 |

{post.author}

100 |
101 |

{post.readingTime}

102 |
103 |
104 |
105 |
106 | ); 107 | 108 | export default BlogList; 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | // src/app/page.tsx 2 | 'use client'; 3 | 4 | import Link from 'next/link'; 5 | import { 6 | FaGithub, 7 | FaMarkdown, 8 | FaMobile, 9 | FaBolt, 10 | FaSearch, 11 | FaImage, 12 | FaCode, 13 | FaRocket, 14 | FaPalette, 15 | FaChartLine 16 | } from 'react-icons/fa'; 17 | import { motion } from 'framer-motion'; 18 | 19 | interface FeatureCardProps { 20 | icon: React.ReactNode; 21 | title: string; 22 | description: string; 23 | } 24 | 25 | const FeatureCard: React.FC = ({ icon, title, description }) => ( 26 | 30 |
{icon}
31 |

{title}

32 |

{description}

33 |
34 | ); 35 | 36 | export default function Home() { 37 | return ( 38 |
39 | {/* Hero Section */} 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 53 | Turn Your GitHub Repo 54 | Into a Beautiful Blog 55 | 56 | 62 | A modern, feature-rich blogging platform that transforms your Markdown files into an elegant blog. 63 | Built with Next.js 14, Tailwind CSS, and the power of GitHub. 64 | 65 | 71 | 75 | View Demo Blog 76 | 77 | 83 | 84 | View on GitHub 85 | 86 | 87 |
88 |
89 |
90 |
91 | 92 | {/* Features Section */} 93 |
94 |
95 |
96 |

97 | Everything You Need for a Modern Blog 98 |

99 |

100 | Built with the latest technologies and best practices, our blogging platform 101 | provides everything you need to create and maintain a professional blog. 102 |

103 |
104 | 105 |
106 | } 108 | title="GitHub-Powered CMS" 109 | description="Use GitHub as your content management system. Write posts in Markdown, manage them with Git, and leverage version control." 110 | /> 111 | } 113 | title="Lightning Fast" 114 | description="Built with Next.js 14 and optimized for performance. Your blog loads instantly and ranks high in Core Web Vitals." 115 | /> 116 | } 118 | title="Beautiful Design" 119 | description="Professionally designed components and layouts that look great out of the box, powered by Tailwind CSS." 120 | /> 121 | } 123 | title="Developer Friendly" 124 | description="Clean code, TypeScript support, and extensive documentation make it easy to customize and extend." 125 | /> 126 | } 128 | title="SEO Optimized" 129 | description="Built-in SEO best practices, meta tags, and structured data to help your content rank better." 130 | /> 131 | } 133 | title="Rich Media Support" 134 | description="Support for images, code snippets, embeds, and more. All optimized for fast loading and great UX." 135 | /> 136 |
137 |
138 |
139 | 140 | {/* CTA Section */} 141 |
142 |
143 |

144 | Ready to Start Your Blog? 145 |

146 |

147 | Transform your GitHub Markdown files into a beautiful, modern blog in minutes. 148 |

149 |
150 | 154 | View Demo Blog 155 | 156 | 162 | Read the Docs 163 | 164 |
165 |
166 |
167 |
168 | ); 169 | } -------------------------------------------------------------------------------- /src/components/BlogPost.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/alt-text */ 2 | /* eslint-disable @next/next/no-img-element */ 3 | // src/components/BlogPost.tsx 4 | 5 | "use client"; 6 | 7 | import React from "react"; 8 | import Link from "next/link"; 9 | import { formatDate } from "@/utils/dateFormatter"; 10 | import { BlogPostProps } from "@/lib/getBlogPosts"; 11 | import { FaGithub, FaTwitter, FaLinkedin } from "react-icons/fa"; 12 | import RelatedPosts from "./RelatedPosts"; 13 | import ErrorBoundary from "./ErrorBoundary"; 14 | import ScrollProgressBar from "./ScrollProgressBar"; 15 | import SocialShare from "./SocialShare"; 16 | import ClientSideImage from "./ClientSideImage"; 17 | 18 | const BlogPost: React.FC<{ 19 | post: BlogPostProps; 20 | relatedPosts: BlogPostProps[]; 21 | }> = ({ post, relatedPosts }) => { 22 | if (!post) { 23 | return ( 24 |
25 | Error: Post data is missing 26 |
27 | ); 28 | } 29 | 30 | const renderHTMLContent = () => { 31 | if (!post.htmlContent) { 32 | return
Error: HTML content is missing
; 33 | } 34 | 35 | return ( 36 |
40 | ); 41 | }; 42 | 43 | return ( 44 |
45 | Error loading header
}> 46 |
47 | 48 | 53 |
54 | 55 | 56 |
57 | {post.coverImage ? ( 58 | 66 | ) : ( 67 |
68 | No cover image available 69 |
70 | )} 71 |
72 | 73 |
74 | Error loading post meta
}> 75 |
76 |
77 | {post.authorImage ? ( 78 | 85 | ) : ( 86 |
87 | ? 88 |
89 | )} 90 |
91 | {post.author && ( 92 |

{post.author}

93 | )} 94 | {post.date && ( 95 |

96 | {formatDate(post.date)} 97 |

98 | )} 99 |
100 |
101 | {post.readingTime && ( 102 |

{post.readingTime}

103 | )} 104 |
105 | 106 | 107 | Error loading post title
}> 108 |

109 | {post.title || "Untitled Post"} 110 |

111 | 112 | 113 | Error loading social share
}> 114 | 115 | 116 | 117 | Error loading post content}> 118 | {renderHTMLContent()} 119 | 120 | 121 | Error loading tags}> 122 | {post.tags && post.tags.length > 0 && ( 123 |
124 |

Tags:

125 |
126 | {post.tags.map((tag) => ( 127 | 131 | {tag} 132 | 133 | ))} 134 |
135 |
136 | )} 137 |
138 | 139 | Error loading author bio}> 140 | {post.author && ( 141 |
142 |
143 | {post.authorImage ? ( 144 | 151 | ) : ( 152 |
153 | ? 154 |
155 | )} 156 |
157 |

{post.author}

158 | {post.authorBio && ( 159 |

{post.authorBio}

160 | )} 161 |
162 |
163 |
164 | )} 165 |
166 | 167 | Error loading related posts}> 168 | {relatedPosts && relatedPosts.length > 0 && ( 169 | 170 | )} 171 | 172 | 173 | 174 | 207 | 208 | ); 209 | }; 210 | 211 | export default BlogPost; 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js GitHub Markdown Blog 2 | 3 | A modern, feature-rich blogging platform that uses GitHub as a CMS. Transform your Markdown files into a beautiful, responsive blog with minimal setup. Perfect for developers who want to keep their content in GitHub and integrate a blog into their Next.js applications. 4 | 5 | ## ✨ Features 6 | 7 | ### Content Management 8 | - 📝 Write posts in Markdown with full GitHub Flavored Markdown support 9 | - 🎨 Frontmatter for rich metadata and customization 10 | - 📑 Automatic category and tag organization 11 | - 📊 Reading time estimation 12 | 13 | ### Design & UI 14 | - 🎯 Responsive, mobile-first design 15 | - 🖼️ Optimized image loading with blur placeholders 16 | - 🌙 Smooth animations and transitions 17 | - 📱 Progressive Web App (PWA) ready 18 | - 🎨 Customizable design system via Tailwind CSS 19 | 20 | ### Performance & SEO 21 | - ⚡ Static Site Generation (SSG) for optimal performance 22 | - 🔍 SEO optimized with meta tags and structured data 23 | - 📱 Mobile performance optimized 24 | - 🖼️ Automatic image optimization 25 | - 🗺️ Automatic sitemap generation 26 | 27 | ### Social & Sharing 28 | - 🔗 Social media sharing buttons 29 | - 👥 Author profiles and bios 30 | - 📊 Reading progress indicator 31 | - ⬆️ Scroll to top button 32 | - 💬 Related posts suggestions 33 | 34 | ## 🚀 Getting Started 35 | 36 | ### Prerequisites 37 | - Node.js 18+ and npm/yarn/bun 38 | - A GitHub account 39 | - A GitHub repository for your blog posts 40 | 41 | ### Basic Setup 42 | 43 | 1. Clone this repository: 44 | ```bash 45 | git clone https://github.com/yourusername/nextjs-github-markdown-blog.git 46 | cd nextjs-github-markdown-blog 47 | ``` 48 | 49 | 2. Install dependencies: 50 | ```bash 51 | npm install # or yarn install or bun install 52 | ``` 53 | 54 | 3. Create a GitHub repository for your blog posts and create a personal access token: 55 | - Go to GitHub Settings -> Developer Settings -> Personal Access Tokens 56 | - Generate a new token with `repo` scope 57 | - Copy the token for the next step 58 | 59 | 4. Create a `.env.local` file: 60 | ```env 61 | # Required 62 | GITHUB_REPO=username/blog-posts-repo 63 | GITHUB_TOKEN=your_github_token_here 64 | 65 | # Optional but recommended 66 | NEXT_PUBLIC_SITE_URL=https://your-blog-domain.com 67 | NEXT_PUBLIC_SITE_NAME=Your Blog Name 68 | NEXT_PUBLIC_SITE_DESCRIPTION=Your blog description 69 | NEXT_PUBLIC_TWITTER_HANDLE=@yourusername 70 | ``` 71 | 72 | 5. Run the development server: 73 | ```bash 74 | npm run dev # or yarn dev or bun dev 75 | ``` 76 | 77 | ### Integrating with Existing Next.js App 78 | 79 | 1. Copy the required directories to your project: 80 | ``` 81 | src/ 82 | ├── components/ # Blog components 83 | ├── lib/ # Blog utilities 84 | └── app/ 85 | └── blog/ # Blog pages 86 | ``` 87 | 88 | 2. Add required dependencies to your `package.json`: 89 | ```json 90 | { 91 | "dependencies": { 92 | "gray-matter": "^4.0.3", 93 | "reading-time": "^1.5.0", 94 | "rehype": "^13.0.2", 95 | "rehype-highlight": "^7.0.1", 96 | "rehype-prism-plus": "2.0.0", 97 | "rehype-raw": "^7.0.0", 98 | "rehype-sanitize": "6.0.0", 99 | "rehype-stringify": "10.0.1", 100 | "remark": "15.0.1", 101 | "remark-gfm": "4.0.0", 102 | "remark-html": "^16.0.1", 103 | "remark-parse": "11.0.0", 104 | "remark-rehype": "11.1.1" 105 | } 106 | } 107 | ``` 108 | 109 | 3. Add the blog routes to your `next.config.mjs`: 110 | ```javascript 111 | const nextConfig = { 112 | // ... your existing config 113 | images: { 114 | domains: [ 115 | 'raw.githubusercontent.com', 116 | 'github.com', 117 | '**.githubusercontent.com' 118 | ], 119 | } 120 | }; 121 | ``` 122 | 123 | 4. Add the required environment variables mentioned above. 124 | 125 | ## 📝 Blog Post Format 126 | 127 | Create your blog posts as Markdown files in your GitHub repository. Here's a complete example showing all supported features: 128 | 129 | ```markdown 130 | --- 131 | title: "Introducing Our New Next.js Blogging System with Tailwind CSS" 132 | date: "2024-09-28" 133 | excerpt: "A look at our sleek and efficient new blogging system built with Next.js and Tailwind CSS, designed for fast, responsive, and visually appealing content delivery." 134 | category: "Web Development" 135 | tags: ["Next.js", "Tailwind CSS", "Blogging", "GitHub"] 136 | coverImage: "https://raw.githubusercontent.com/Dicklesworthstone/yto_blog_posts/refs/heads/main/blog_01_banner.webp" 137 | author: "Jeffrey Emanuel" 138 | authorImage: "https://pbs.twimg.com/profile_images/1225476100547063809/53jSWs7z_400x400.jpg" 139 | authorBio: "Software Engineer and Founder of YouTube Transcript Optimizer" 140 | --- 141 | 142 | # Introducing Our New Next.js Blogging System with Tailwind CSS 143 | 144 | ## A Sleek and Efficient Way to Share Your Thoughts 145 | 146 | ![Blog System Banner](https://raw.githubusercontent.com/Dicklesworthstone/yto_blog_posts/refs/heads/main/blog_01_banner.webp) 147 | 148 | Hello, fellow developers and tech enthusiasts! Today, I'm excited to introduce our brand new blogging system built with Next.js and styled with Tailwind CSS. This powerful combination allows for a fast, responsive, and visually appealing blog that's easy to maintain and expand. 149 | 150 | ### Key Features 151 | 152 | 1. **GitHub-Powered Content**: All our blog posts are stored as Markdown files in a public GitHub repository. This means version control and collaboration are built right in! 153 | 154 | 2. **Automatic Updates**: The system periodically checks for new Markdown files in the repo, ensuring that new posts are automatically added to the blog without manual intervention. 155 | 156 | 3. **Responsive Design**: Thanks to Tailwind CSS, our blog looks great on devices of all sizes. From mobile phones to wide-screen desktops, the reading experience is always optimal. 157 | 158 | 4. **Fast Loading**: Next.js's static site generation capabilities mean that our blog pages load incredibly quickly, providing a smooth user experience. 159 | 160 | 5. **Rich Markdown Support**: We support GitHub Flavored Markdown, allowing for a wide range of formatting options. Let's test some of them: 161 | 162 | - *Italic* and **bold** text 163 | - [Links to external sites](https://nextjs.org) 164 | - Lists (like this one!) 165 | - And even code blocks: 166 | 167 | ```javascript 168 | const getBlogPosts = async () => { 169 | const response = await fetch('https://api.github.com/repos/user/blog-posts/contents'); 170 | const files = await response.json(); 171 | return files.filter(file => file.name.endsWith('.md')); 172 | }; 173 | ``` 174 | 175 | 6. **SEO Optimized**: Each blog post comes with customizable metadata, ensuring that our content is easily discoverable by search engines. 176 | 177 | ### How It Works 178 | 179 | Our blogging system leverages the power of Next.js's API routes and static site generation. Here's a simplified overview of the process: 180 | 181 | 1. Markdown files are added to our GitHub repository. 182 | 2. Our Next.js app periodically fetches the list of files from the GitHub API. 183 | 3. When a new file is detected, the app fetches its content and processes the Markdown. 184 | 4. The processed content is then rendered using our custom React components, styled with Tailwind CSS. 185 | 5. Next.js generates static pages for each blog post, ensuring fast load times. 186 | 187 | ### What's Next? 188 | 189 | We're constantly working on improving our blogging system. Some features we're considering for future updates include: 190 | 191 | - Comment system integration 192 | - Social media sharing buttons 193 | - Dark mode toggle 194 | - RSS feed generation 195 | 196 | ## Conclusion 197 | 198 | This new blogging system represents a significant step forward in our content management capabilities. It combines the simplicity of Markdown with the power of modern web technologies to create a blogging experience that's enjoyable for both writers and readers. 199 | 200 | We're excited to use this system for sharing more updates, tutorials, and thoughts with you all. Stay tuned for more posts coming soon! 201 | 202 | --- 203 | 204 | *This post was written in Markdown and automatically rendered by our Next.js blogging system. Pretty cool, huh?* 205 | ``` 206 | 207 | ### Frontmatter Fields Explained 208 | 209 | The frontmatter section at the top of each Markdown file (between the `---` markers) defines the post's metadata: 210 | 211 | #### Required Fields 212 | - `title`: The main title of your blog post 213 | - `date`: Publication date in "YYYY-MM-DD" format 214 | - `excerpt`: A brief description used in previews and SEO 215 | - `category`: Main category for the post 216 | 217 | #### Optional Fields 218 | - `tags`: Array of related topics for categorization 219 | - `coverImage`: URL to the post's hero image 220 | - `author`: Name of the post's author 221 | - `authorImage`: URL to the author's profile picture 222 | - `authorBio`: Brief author biography 223 | 224 | ### Supported Markdown Features 225 | 226 | The blog system supports all standard Markdown features plus GitHub Flavored Markdown (GFM) extensions: 227 | 228 | - Headers (H1-H6) 229 | - Bold and italic text 230 | - Ordered and unordered lists 231 | - Code blocks with syntax highlighting 232 | - Tables 233 | - Blockquotes 234 | - Images 235 | - Links 236 | - Task lists 237 | - Strikethrough text 238 | - Emoji shortcodes 239 | - Automatic URL linking 240 | - Footnotes 241 | 242 | ### Images and Assets 243 | 244 | Images can be referenced in two ways: 245 | 1. Direct URLs to images hosted anywhere 246 | 2. Relative paths to images stored in your blog posts repository 247 | 248 | For optimal performance, we recommend: 249 | - Using WebP format for images 250 | - Hosting images in your GitHub repository alongside posts 251 | - Keeping image files under 1MB when possible 252 | - Using descriptive alt text for accessibility 253 | 254 | ### File Naming Convention 255 | 256 | We recommend naming your blog post files using kebab-case with a date prefix: 257 | ``` 258 | 2024-09-28-introducing-nextjs-blog-system.md 259 | ``` 260 | 261 | This helps with: 262 | - Chronological ordering 263 | - URL slugs 264 | - File organization 265 | - Post identification 266 | 267 | ## 🎨 Customization 268 | 269 | ### Design System 270 | 271 | Modify `tailwind.config.ts` to customize your design: 272 | 273 | ```typescript 274 | theme: { 275 | extend: { 276 | colors: { 277 | primary: { 278 | // Your color palette 279 | }, 280 | }, 281 | typography: { 282 | // Your typography scale 283 | }, 284 | // ... other customizations 285 | } 286 | } 287 | ``` 288 | 289 | ### Components 290 | 291 | All components are in `src/components/`. Key components you might want to customize: 292 | 293 | - `BlogPost.tsx`: Individual post layout 294 | - `BlogList.tsx`: Post list/grid layout 295 | - `RelatedPosts.tsx`: Related posts section 296 | - `SocialShare.tsx`: Social sharing buttons 297 | 298 | ### Advanced Configuration 299 | 300 | Create a `blog.config.ts` file for additional settings: 301 | 302 | ```typescript 303 | export default { 304 | postsPerPage: 10, 305 | featuredPostsCount: 3, 306 | defaultAuthor: { 307 | name: 'Your Name', 308 | image: '/images/default-avatar.jpg', 309 | bio: 'Your bio' 310 | }, 311 | social: { 312 | twitter: 'yourusername', 313 | github: 'yourusername', 314 | linkedin: 'yourusername' 315 | } 316 | }; 317 | ``` 318 | 319 | ## 🚀 Deployment 320 | 321 | ### Vercel (Recommended) 322 | 1. Push your code to GitHub 323 | 2. Import your repository on Vercel 324 | 3. Add environment variables 325 | 4. Deploy! 326 | 327 | ### Other Platforms 328 | The blog supports any platform that can host Next.js applications: 329 | 330 | - Netlify 331 | - AWS Amplify 332 | - Digital Ocean 333 | - Self-hosted 334 | 335 | ## 📈 Analytics Integration 336 | 337 | The blog is prepared for various analytics solutions: 338 | 339 | ```typescript 340 | // Add to src/app/layout.tsx 341 | import { GoogleAnalytics } from '@next/third-parties/google' 342 | 343 | export default function RootLayout({ children }) { 344 | return ( 345 | 346 | 347 | 348 | 349 | {children} 350 | 351 | ) 352 | } 353 | ``` 354 | 355 | ## 📄 License 356 | 357 | MIT - feel free to use this project however you'd like. 358 | 359 | ## 🙏 Acknowledgments 360 | 361 | This project is built with and inspired by: 362 | - [Next.js](https://nextjs.org/) 363 | - [Tailwind CSS](https://tailwindcss.com/) 364 | - [React Icons](https://react-icons.github.io/react-icons/) 365 | - And many other open-source projects 366 | 367 | --- 368 | 369 | Thanks for your interest in my open-source project! I hope you find it useful. You might also find my commercial web apps useful, and I would really appreciate it if you checked them out: 370 | 371 | **[YoutubeTranscriptOptimizer.com](https://youtubetranscriptoptimizer.com)** makes it really quick and easy to paste in a YouTube video URL and have it automatically generate not just a really accurate direct transcription, but also a super polished and beautifully formatted written document that can be used independently of the video. 372 | 373 | The document basically sticks to the same material as discussed in the video, but it sounds much more like a real piece of writing and not just a transcript. It also lets you optionally generate quizzes based on the contents of the document, which can be either multiple choice or short-answer quizzes, and the multiple choice quizzes get turned into interactive HTML files that can be hosted and easily shared, where you can actually take the quiz and it will grade your answers and score the quiz for you. 374 | 375 | **[FixMyDocuments.com](https://fixmydocuments.com/)** lets you submit any kind of document— PDFs (including scanned PDFs that require OCR), MS Word and Powerpoint files, images, audio files (mp3, m4a, etc.) —and turn them into highly optimized versions in nice markdown formatting, from which HTML and PDF versions are automatically generated. Once converted, you can also edit them directly in the site using the built-in markdown editor, where it saves a running revision history and regenerates the PDF/HTML versions. 376 | 377 | In addition to just getting the optimized version of the document, you can also generate many other kinds of "derived documents" from the original: interactive multiple-choice quizzes that you can actually take and get graded on; slick looking presentation slides as PDF or HTML (using LaTeX and Reveal.js), an in-depth summary, a concept mind map (using Mermaid diagrams) and outline, custom lesson plans where you can select your target audience, a readability analysis and grade-level versions of your original document (good for simplifying concepts for students), Anki Flashcards that you can import directly into the Anki app or use on the site in a nice interface, and more. 378 | --------------------------------------------------------------------------------