commit 81bde603a6db878299d01c0f0907241ac3e959a0 Author: joker Date: Sat Sep 9 23:08:20 2023 +0300 new file: .eslintrc.json new file: .github/workflows/ci.yml new file: .gitignore new file: .prettierignore new file: .storybook/main.ts new file: .storybook/preview.tsx new file: LICENSE.md new file: README.md new file: app/[...slug]/not-found.tsx new file: app/[...slug]/page.tsx new file: app/layout.tsx new file: app/loading.tsx new file: app/og/route.tsx new file: app/page.tsx new file: app/posts/[...slug]/not-found.tsx new file: app/posts/[...slug]/page.tsx new file: app/posts/page.tsx new file: components/analytics.tsx new file: components/blog-title.tsx new file: components/button.tsx new file: components/callout.tsx new file: components/comments.tsx diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..58494f0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["next/core-web-vitals", "plugin:storybook/recommended"], + "plugins": ["eslint-plugin-tailwindcss"], + "rules": { + "tailwindcss/classnames-order": ["error", { "callees": ["cn"] }], + "tailwindcss/no-custom-classname": ["error", { "callees": ["cn"] }], + "tailwindcss/no-contradicting-classname": ["error", { "callees": ["cn"] }], + "tailwindcss/enforces-shorthand": ["error", { "callees": ["cn"] }] + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4da0e88 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: 'CI' + +on: + pull_request: + push: + branches: ['prod'] + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: ['ubuntu-latest'] + node-version: ['16.x'] + steps: + - name: 'Checkout repository' + uses: actions/checkout@v3 + + - name: 'Use Node.js v${{ matrix.node-version }}' + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: 'Install pnpm' + uses: pnpm/action-setup@v2 + with: + version: 6.0.2 + + - name: 'Install Dependencies' + run: pnpm install + + - name: 'Build Contentlayer' + run: NODE_ENV=production npx contentlayer build + + - name: 'Run Type Checks' + run: npx tsc --noEmit + + - name: 'Run Lint and Format Checks' + run: pnpm run style:all + + - name: 'Run Tests' + run: pnpm run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bde1d80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem +.vscode +.contentlayer + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..bde1d80 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem +.vscode +.contentlayer + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..86946b9 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,17 @@ +import { type StorybookConfig } from '@storybook/nextjs'; + +const config: StorybookConfig = { + stories: ['../components/stories/**/*.stories.@(js|jsx|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + 'storybook-tailwind-dark-mode', + ], + framework: { + name: '@storybook/nextjs', + options: {}, + }, +}; + +export default config; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..258097c --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,66 @@ +import '@/styles/globals.css'; +import 'react-tooltip/dist/react-tooltip.css'; +import { Red_Hat_Display } from 'next/font/google'; +import type { Decorator, Parameters } from '@storybook/react'; +import type { GlobalTypes } from '@storybook/types'; + +import { cn } from '@/lib/utils'; + +const fontSans = Red_Hat_Display({ + subsets: ['latin'], + variable: '--font-red-hat', +}); + +export const parameters: Parameters = { + nextjs: { appDirectory: true }, + layout: 'fullscreen', + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; + +export const globalTypes: GlobalTypes = { + darkMode: { + type: 'boolean', + defaultValue: false, + }, +}; + +export const decorators: Decorator[] = [ + (Story) => ( +
+
+
+ +
+ + ), +]; diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5c64afe --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Kfir Fitousi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..455719a --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +[![‹kfir/blog›](https://user-images.githubusercontent.com/37262772/213866538-70e7024e-64f5-4bd8-ba92-af803e6169e7.png)](https://blog.kfirfitousi.com) +![Build Status](https://img.shields.io/github/deployments/kfirfitousi/blog/Production%20%E2%80%93%20blog?label=build&logo=vercel&style=for-the-badge) +![Website Status](https://img.shields.io/website?down_color=lightgrey&logo=vercel&style=for-the-badge&url=https%3A%2F%2Fblog.kfirfitousi.com) +![CI Status](https://img.shields.io/github/actions/workflow/status/kfirfitousi/blog/ci.yml?branch=main&label=CI&logo=github&style=for-the-badge) +![License](https://img.shields.io/github/license/kfirfitousi/blog?color=blue&style=for-the-badge) + +Blog built with Next.js 13, TypeScript, Contentlayer, Zustand and TailwindCSS. + +- 🔥 Using latest Next.js 13 features including the `/app` directory, SEO & Metadata, `next/font`, and React 18's Server Components +- 🎛 Customizable reading experience - light/dark mode, serif/sans-serif, and font size +- 🧩 MDX plugins and custom components +- 💬 Comment sections using [Giscus](https://giscus.app/) +- ⚡️ Vercel OG image generation at the Edge +- 📖 Storybook v7 integration (published to [story.blog.kfirfitousi.com](https://story.blog.kfirfitousi.com)) diff --git a/app/[...slug]/not-found.tsx b/app/[...slug]/not-found.tsx new file mode 100644 index 0000000..b5f63cb --- /dev/null +++ b/app/[...slug]/not-found.tsx @@ -0,0 +1,15 @@ +import { Home, XCircle } from 'lucide-react'; + +import { Button } from '@/components/button'; + +export default function NotFound() { + return ( +
+ +

+ Page not found +

+
+ ); +} diff --git a/app/[...slug]/page.tsx b/app/[...slug]/page.tsx new file mode 100644 index 0000000..74702dc --- /dev/null +++ b/app/[...slug]/page.tsx @@ -0,0 +1,64 @@ +import { notFound } from 'next/navigation'; +import { type Metadata } from 'next/types'; +import { allPages } from 'contentlayer/generated'; + +import { blogConfig } from '@/config'; +import { MDXContent } from '@/components/mdx-content'; + +type PageProps = { + params: { + slug: string[]; + }; +}; + +export async function generateStaticParams(): Promise { + return allPages.map(({ slug }) => ({ + slug: slug.split('/'), + })); +} + +export function generateMetadata({ params }: PageProps): Metadata { + const { title, description, url } = allPages.find( + ({ slug }) => slug === params.slug.join('/'), + ) || { + title: 'Page Not Found', + description: 'Page Not Found', + url: '/', + }; + + const ogImage = { + url: `${blogConfig.url}/og?title=${title}`, + }; + + return { + title, + description, + openGraph: { + type: 'website', + url: `${blogConfig.url}${url}`, + title, + description, + images: [ogImage], + }, + twitter: { + title, + description, + images: ogImage, + card: 'summary_large_image', + }, + }; +} + +export default async function Page({ params }: PageProps) { + const page = allPages.find(({ slug }) => slug === params.slug.join('/')); + + if (!page) { + notFound(); + } + + return ( +
+ +
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..c21f59c --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,117 @@ +import '@/styles/globals.css'; +import 'react-tooltip/dist/react-tooltip.css'; +import { Newsreader, Red_Hat_Display } from 'next/font/google'; +import { type Metadata } from 'next/types'; +import { allPosts } from 'contentlayer/generated'; + +import { blogConfig } from '@/config'; +import { Analytics } from '@/components/analytics'; +import { FontStyleProvider } from '@/components/font-style-provider'; +import { Footer } from '@/components/footer'; +import { Header } from '@/components/header'; +import { Search } from '@/components/search'; +import { cn } from '@/lib/utils'; + +const fontSans = Red_Hat_Display({ + subsets: ['latin'], + variable: '--font-red-hat', +}); + +const fontSerif = Newsreader({ + subsets: ['latin'], + variable: '--font-newsreader', +}); + +type RootLayoutProps = { + children: React.ReactNode; +}; + +export const metadata: Metadata = { + title: { + default: blogConfig.title, + template: `${blogConfig.title} | %s`, + }, + openGraph: { + title: { + default: blogConfig.title, + template: `${blogConfig.title} | %s`, + }, + }, + twitter: { + title: { + default: blogConfig.title, + template: `${blogConfig.title} | %s`, + }, + }, + robots: { + index: true, + follow: true, + }, + icons: [ + { + rel: 'apple-touch-icon', + sizes: '180x180', + url: '/apple-touch-icon.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '32x32', + url: '/favicon-32x32.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + url: '/favicon-16x16.png', + }, + ], +}; + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + + + +
+
+
+
+ {children} +
+
+
+
+