;
10 | }
11 |
12 | function PostNavigation({ posts }: PostNavigationProps) {
13 | posts.sort((a, b) => {
14 | return new Date(b.time.created).getTime() - new Date(a.time.created).getTime();
15 | });
16 |
17 | const currentSlug = usePathname().split("/").pop();
18 | const currentIndex = posts.findIndex((post) => post.slug === currentSlug);
19 | const previous = currentIndex < posts.length - 1 ? posts[currentIndex + 1] : null;
20 | const next = currentIndex > 0 ? posts[currentIndex - 1] : null;
21 |
22 | if (!previous && !next) {
23 | return null;
24 | }
25 |
26 | return (
27 |
28 | {previous && (
29 |
30 | Previous
31 | {previous.title}
32 |
33 | )}
34 | {next && (
35 |
36 | Next
37 | {next.title}
38 |
39 | )}
40 |
41 | );
42 | }
43 |
44 | export { PostNavigation };
45 |
--------------------------------------------------------------------------------
/components/breadcrumb/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/cn";
4 |
5 | import { ChevronRightIcon } from "@radix-ui/react-icons";
6 | import { Link } from "next-view-transitions";
7 | import { usePathname } from "next/navigation";
8 | import React from "react";
9 |
10 | export const Breadcrumb = () => {
11 | const pathname = usePathname();
12 |
13 | const paths = pathname
14 | .split("/")
15 | .filter((path) => path !== "")
16 | .map((path) => path.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()));
17 |
18 | return (
19 |
20 |
21 | Home
22 |
23 |
24 | {paths.map((path, index) => {
25 | const href = `/${paths
26 | .slice(0, index + 1)
27 | .join("/")
28 | .toLowerCase()}`;
29 |
30 | const isLast = index === paths.length - 1;
31 |
32 | return (
33 |
34 | {isLast ? (
35 | {path}
36 | ) : (
37 |
38 | {path}
39 |
40 | )}
41 | {index < paths.length - 1 && }
42 |
43 | );
44 | })}
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/components/screens/posts/index.tsx:
--------------------------------------------------------------------------------
1 | import type { Post } from "@/types";
2 |
3 | import { TableOfContents } from "@/components/on-this-page";
4 | import { PostNavigation } from "@/components/post-navigation";
5 | import { formatter } from "@/lib/formatter";
6 | import { getPosts } from "@/lib/mdx";
7 | import { MDX } from "@/mdx-components";
8 |
9 | import React from "react";
10 | import { readingTime } from "reading-time-estimator";
11 |
12 | interface Props {
13 | post: Post;
14 | route: string;
15 | }
16 |
17 | export const Layout = ({ post, route }: Props) => {
18 | const posts = getPosts(route);
19 |
20 | const Seperator = () => {
21 | return ⋅
;
22 | };
23 |
24 | const PublishedTime = () => {
25 | return Published {formatter.date(new Date(post.time.created))}
;
26 | };
27 | const UpdateTime = () => {
28 | return Updated {formatter.date(new Date(post.time.updated))}
;
29 | };
30 |
31 | const ReadingTime = () => {
32 | return {readingTime(post.content).minutes} minutes read
;
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 |
{post.title}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/components/theme/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type React from "react";
4 |
5 | import { cn } from "@/lib/cn";
6 |
7 | import { Monitor, Moon, Sun } from "lucide-react";
8 | import { ThemeProvider, useTheme } from "next-themes";
9 | import { useEffect, useState } from "react";
10 |
11 | export const AppThemeSwitcher = () => {
12 | const [mounted, setMounted] = useState(false);
13 | const { theme, setTheme } = useTheme();
14 |
15 | useEffect(() => {
16 | setMounted(true);
17 | }, []);
18 |
19 | if (!mounted) return null;
20 |
21 | const buttons = [
22 | {
23 | label: "system",
24 | icon: ,
25 | active: theme === "system",
26 | },
27 | { label: "dark", icon: , active: theme === "dark" },
28 | { label: "light", icon: , active: theme === "light" },
29 | ];
30 |
31 | return (
32 |
33 | {buttons.map(({ label, icon, active }) => (
34 | setTheme(label)}
38 | className={cn("ransition-all flex h-6 w-6 items-center justify-center rounded-[4px] hover:opacity-50", active ? "bg-gray-4 text-foreground" : "")}
39 | >
40 | {icon}
41 |
42 | ))}
43 |
44 | );
45 | };
46 |
47 | export const AppThemeProvider = ({
48 | children,
49 | }: {
50 | children: React.ReactNode;
51 | }) => {
52 | return (
53 |
54 | {children}
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/public/sitemap-0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://next-sylph-portfolio.vercel.app 2024-10-17T11:14:06.574Z daily 0.7
4 | https://next-sylph-portfolio.vercel.app/examples 2024-10-17T11:14:06.575Z daily 0.7
5 | https://next-sylph-portfolio.vercel.app/guides 2024-10-17T11:14:06.575Z daily 0.7
6 | https://next-sylph-portfolio.vercel.app/icon 2024-10-17T11:14:06.575Z daily 0.7
7 | https://next-sylph-portfolio.vercel.app/examples/component-showcase 2024-10-17T11:14:06.575Z daily 0.7
8 | https://next-sylph-portfolio.vercel.app/guides/basic-writing-and-formatting-syntax 2024-10-17T11:14:06.575Z daily 0.7
9 | https://next-sylph-portfolio.vercel.app/guides/getting-started 2024-10-17T11:14:06.575Z daily 0.7
10 | https://next-sylph-portfolio.vercel.app/guides/project-structure 2024-10-17T11:14:06.575Z daily 0.7
11 |
--------------------------------------------------------------------------------
/scripts/update-mdx-timestamps.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | const updateTimestamps = (filePath, overrideCreated = false) => {
5 | const mdxContent = fs.readFileSync(filePath, "utf-8");
6 | const frontmatterRegex = /---\n([\s\S]*?)\n---/;
7 | const match = frontmatterRegex.exec(mdxContent);
8 |
9 | if (match) {
10 | let frontmatter = match[1];
11 |
12 | const fileStats = fs.statSync(filePath);
13 | const created = fileStats.birthtime.toISOString();
14 | const updated = fileStats.mtime.toISOString();
15 |
16 | if (overrideCreated || !frontmatter.includes("created:")) {
17 | if (frontmatter.includes("created:")) {
18 | frontmatter = frontmatter.replace(/created: ".*"/, `created: "${created}"`);
19 | } else if (frontmatter.includes("time:")) {
20 | frontmatter = frontmatter.replace(/time:\n\s*created: ".*"/, `time:\n created: "${created}"`);
21 | } else {
22 | frontmatter += `\ntime:\n created: "${created}"`;
23 | }
24 | }
25 |
26 | if (frontmatter.includes("updated:")) {
27 | frontmatter = frontmatter.replace(/updated: ".*"/, `updated: "${updated}"`);
28 | } else if (frontmatter.includes("time:")) {
29 | frontmatter = frontmatter.replace(/time:\n\s*created: ".*"/, `time:\n created: "${created}"\n updated: "${updated}"`);
30 | } else {
31 | frontmatter += `\ntime:\n updated: "${updated}"`;
32 | }
33 |
34 | const newContent = mdxContent.replace(frontmatterRegex, `---\n${frontmatter}\n---`);
35 |
36 | fs.writeFileSync(filePath, newContent, "utf-8");
37 | }
38 | };
39 |
40 | const findAllMdxFiles = (dirPath, overrideCreated = false) => {
41 | const files = fs.readdirSync(dirPath);
42 |
43 | for (const file of files) {
44 | const fullPath = path.join(dirPath, file);
45 |
46 | if (fs.statSync(fullPath).isDirectory()) {
47 | findAllMdxFiles(fullPath, overrideCreated);
48 | } else if (fullPath.endsWith(".mdx")) {
49 | updateTimestamps(fullPath, overrideCreated);
50 | }
51 | }
52 | };
53 |
54 | const overrideCreated = process.argv.includes("--override-created");
55 |
56 | findAllMdxFiles(path.join(__dirname, "../app"), overrideCreated);
57 |
--------------------------------------------------------------------------------
/app/(posts)/guides/posts/getting-started.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Getting Started"
3 | author:
4 | name: "Raphael Salaja"
5 | link: "https://twitter.com/raphaelsalaja"
6 | handle: "raphaelsalaja"
7 | time:
8 | created: "2024-10-12T20:54:05.150Z"
9 | updated: "2024-10-17T09:56:06.854Z"
10 | ---
11 |
12 | # Installation
13 |
14 | Follow these simple steps to get started:
15 |
16 | ```bash
17 | pnpm install
18 | pnpm dev
19 | ```
20 |
21 | This will install the necessary dependencies and start the development server. Once the server is running, you can view your portfolio at `http://localhost:3000`.
22 |
23 | # Environment Variables
24 |
25 | The following environment variables are required to run the application:
26 |
27 | - `NEXT_PUBLIC_SITE_URL`: The URL of your website.
28 |
29 | Run the following command to create a `.env.local` file and add the required environment variables:
30 |
31 | ```bash
32 | vercel env pull
33 | ```
34 |
35 | Then, update the values in the `.env.local` file with your own values.
36 |
37 | # Scripts
38 |
39 | The following scripts are available to help you manage development, build processes, and linting:
40 |
41 | `build`
42 |
43 | Runs the linting process, updates MDX file timestamps, and then builds the Next.js application for production. This ensures code quality and proper SEO metadata before deployment.
44 |
45 | ```bash
46 | pnpm build
47 | ```
48 |
49 | `postbuild`
50 |
51 | Automatically generates the sitemap after the build process completes. This script uses the `next-sitemap` package to create a `sitemap.xml` file for better SEO and discoverability.
52 |
53 | ```bash
54 | pnpm postbuild
55 | ```
56 |
57 | `mdx:timestamps`
58 |
59 | Runs a custom Node.js script that updates timestamps (`created` and `updated`) in your MDX files, automating the time management for blog posts.
60 |
61 | ```bash
62 | pnpm mdx:timestamps
63 | ```
64 |
65 | `lint:style`
66 |
67 | Runs `stylelint` on all CSS files to check and automatically fix any style issues. This ensures consistent and clean CSS code.
68 |
69 | ```bash
70 | pnpm lint:style
71 | ```
72 |
73 | `lint:biome`
74 |
75 | Runs the Biome linter to check and fix JavaScript/TypeScript code. This keeps your codebase clean and error-free.
76 |
77 | ```bash
78 | pnpm lint:biome
79 | ```
80 |
81 | `lint:prettier`
82 |
83 | Runs Prettier to format your code according to your project's style guide. This improves code readability and consistency.
84 |
85 | ```bash
86 | pnpm lint:prettier
87 | ```
88 |
89 | `lint`
90 |
91 | A combination of the style, Prettier, and Biome linting scripts, run together to ensure the entire codebase follows consistent style and format rules.
92 |
93 | ```bash
94 | pnpm lint
95 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "npm run lint && npm run mdx:timestamps && next build",
8 | "start": "next start",
9 | "postbuild": "next-sitemap",
10 | "mdx:timestamps": "node ./scripts/update-mdx-timestamps",
11 | "lint:style": "npx stylelint \"**/*.css\" --fix",
12 | "lint:biome": "npx biome check --fix --unsafe .",
13 | "lint:prettier": "npx prettier . --write --log-level warn",
14 | "lint": "npm run lint:style && npm run lint:prettier && npm run lint:biome"
15 | },
16 | "dependencies": {
17 | "@mdx-js/loader": "^3.0.1",
18 | "@mdx-js/mdx": "^3.0.1",
19 | "@mdx-js/react": "^3.0.1",
20 | "@next/mdx": "^14.2.15",
21 | "@radix-ui/colors": "^3.0.0",
22 | "@radix-ui/react-dialog": "^1.1.2",
23 | "@radix-ui/react-dropdown-menu": "^2.1.2",
24 | "@radix-ui/react-icons": "^1.3.0",
25 | "@rehype-pretty/transformers": "^0.13.2",
26 | "@types/mdx": "^2.0.13",
27 | "@vercel/edge": "^1.1.2",
28 | "@vercel/functions": "^1.4.2",
29 | "clsx": "^2.1.1",
30 | "create-stylelint": "^0.5.0",
31 | "framer-motion": "^11.11.9",
32 | "fs": "^0.0.2",
33 | "gray-matter": "^4.0.3",
34 | "highlight.js": "^11.10.0",
35 | "lucide": "^0.452.0",
36 | "lucide-react": "^0.452.0",
37 | "next": "14.2.15",
38 | "next-mdx-remote": "^5.0.0",
39 | "next-sitemap": "^4.2.3",
40 | "next-themes": "^0.3.0",
41 | "next-view-transitions": "^0.3.2",
42 | "path": "^0.12.7",
43 | "postcss-nested": "^6.2.0",
44 | "react": "^18.3.1",
45 | "react-dom": "^18.3.1",
46 | "reading-time-estimator": "^1.11.0",
47 | "rehype-highlight": "^7.0.0",
48 | "rehype-pretty-code": "^0.14.0",
49 | "rehype-slug": "^6.0.0",
50 | "remark-gfm": "^4.0.0",
51 | "remark-rehype": "^11.1.1",
52 | "remark-toc": "^9.0.0",
53 | "shiki": "^1.22.0",
54 | "tailwind-highlightjs": "^2.0.1",
55 | "tailwind-merge": "^2.5.4",
56 | "unified": "^11.0.5"
57 | },
58 | "devDependencies": {
59 | "@biomejs/biome": "1.9.3",
60 | "@ianvs/prettier-plugin-sort-imports": "^4.3.1",
61 | "@tailwindcss/typography": "^0.5.15",
62 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
63 | "@types/node": "^22.7.6",
64 | "@types/react": "^18.3.11",
65 | "@types/react-dom": "^18.3.1",
66 | "eslint": "^9.12.0",
67 | "eslint-config-next": "14.2.15",
68 | "postcss": "^8.4.47",
69 | "prettier": "3.3.3",
70 | "stylelint": "^16.10.0",
71 | "stylelint-config-recess-order": "^5.1.1",
72 | "stylelint-config-standard": "^36.0.1",
73 | "stylelint-order": "^6.0.4",
74 | "tailwindcss": "^3.4.14",
75 | "typescript": "^5.6.3"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/api/og/route.tsx:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from "next/og";
2 |
3 | export const runtime = "edge";
4 |
5 | type Parameters = {
6 | title?: string;
7 | };
8 |
9 | /*
10 | * To assist with generating dynamic Open Graph (OG) images, you can use the Vercel @vercel/og library to compute and generate social card images using Vercel Edge Functions.
11 | * @see: https://vercel.com/docs/functions/og-image-generation
12 | *
13 | * You can use the following code sample to explore using parameters and different content types with next/og.
14 | * @see: https://vercel.com/guides/dynamic-text-as-image
15 | *
16 | * For this example we are going to generate a simple social card image with a dynamic title.
17 | */
18 | export async function GET(request: Request) {
19 | try {
20 | /*
21 | * Next we are going to extract the parameters from the request URL.
22 | */
23 | const { searchParams } = new URL(request.url);
24 | const parameters: Parameters = Object.fromEntries(searchParams);
25 | const { title } = parameters;
26 | console.log(parameters);
27 |
28 | /*
29 | * Finally we are fetching the font file from the public directory.
30 | */
31 | const inter = fetch(new URL("/public/assets/inter/regular.ttf", import.meta.url)).then((res) => res.arrayBuffer());
32 |
33 | return new ImageResponse(
34 |
50 |
58 |
next-sylph-portfolio
59 | {title &&
/
}
60 | {title ? (
61 |
{title.toLowerCase()}
62 | ) : (
63 |
64 |
65 |
66 | )}
67 |
68 |
,
69 | {
70 | width: 1200,
71 | height: 600,
72 | fonts: [
73 | {
74 | name: "Inter",
75 | data: await inter,
76 | weight: 400,
77 | },
78 | ],
79 | },
80 | );
81 | } catch {
82 | return new Response("Failed to generate the image", {
83 | status: 500,
84 | });
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/components/deploy/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DeployLink } from "@/lib/deploy";
4 |
5 | import { motion } from "framer-motion";
6 | import Link from "next/link";
7 |
8 | export const DeployButton = () => {
9 | console.log(DeployLink);
10 | return (
11 |
31 |
32 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | A minimal and lightweight portfolio template designed for developers, designers, and creatives. It offers a clean, modern interface to showcase your work, share your ideas, and write blog posts.
21 |
22 | Built using Next.js, Sylph is optimized for performance and developer experience, providing flexibility and ease of use whether you're showcasing projects or writing content.
23 |
24 | ## Features
25 |
26 | - **Responsive Design**: Works seamlessly on all devices and browsers.
27 | - **MDX and Markdown Support**: Write posts using MDX or Markdown, with extensive flexibility.
28 | - **Optimized for SEO**: Includes sitemaps, robots.txt, and metadata for better search engine visibility.
29 | - **Dynamic OpenGraph (OG) Images**: Automatically generate OG images for social media sharing.
30 | - **Syntax Highlighting**: Built-in support for highlighting code blocks.
31 | - **Tailwind v4**: Fully configured with the latest version of Tailwind CSS for efficient styling.
32 | - **Automated Blog Time Dating**: Automatically manage post creation and updated timestamps.
33 | - **Extensive Frontmatter**: Customize posts with rich metadata and organizational fields.
34 | - **Clean and Simple Structure**: Easy-to-navigate codebase for efficient customization.
35 | - **Light and Dark Mode**: Simple theming with light/dark mode toggle support.
36 | - **Foundations for Expansion**: Built with flexibility in mind, allowing easy expansion and customization.
37 | - **Theming**: Easily extend or customize themes to suit your brand.
38 |
39 | ## Documentation
40 |
41 | To get started with Sylph, check out the [Guides](https://next-sylph-portfolio.vercel.app/guides).
42 |
43 | ## Sponsor
44 |
45 | If you find this project helpful, consider supporting the project. Your contribution helps maintain the project and supports future development.
46 |
--------------------------------------------------------------------------------
/components/on-this-page/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/cn";
4 |
5 | import { motion } from "framer-motion";
6 | import React, { useCallback, useEffect, useState } from "react";
7 |
8 | export const TableOfContents = () => {
9 | const [headings, setHeadings] = useState<{ id: string; text: string; level: string }[]>([]);
10 | const [visibleHeadings, setVisibleHeadings] = useState>(new Set());
11 |
12 | const getHeadings = useCallback(() => {
13 | return Array.from(document.querySelectorAll("h1, h2, h3"))
14 | .filter((heading) => heading.id)
15 | .map((heading) => ({
16 | id: heading.id,
17 | text: heading.textContent || "",
18 | level: heading.tagName.toLowerCase(),
19 | top: (heading as HTMLElement).offsetTop,
20 | }));
21 | }, []);
22 |
23 | useEffect(() => {
24 | const collectedHeadings = getHeadings();
25 | setHeadings(collectedHeadings);
26 |
27 | const observerOptions = {
28 | root: null,
29 | threshold: 0,
30 | };
31 |
32 | const handleIntersection = (entries: IntersectionObserverEntry[]) => {
33 | const visibleSet = new Set(visibleHeadings);
34 |
35 | for (const entry of entries) {
36 | const headingId = entry.target.id;
37 |
38 | if (entry.isIntersecting) {
39 | visibleSet.add(headingId);
40 | } else {
41 | visibleSet.delete(headingId);
42 | }
43 | }
44 |
45 | setVisibleHeadings(new Set(visibleSet));
46 | };
47 |
48 | const observer = new IntersectionObserver(handleIntersection, observerOptions);
49 |
50 | for (const heading of collectedHeadings) {
51 | const element = document.getElementById(heading.id);
52 | if (element) observer.observe(element);
53 | }
54 |
55 | return () => {
56 | observer.disconnect();
57 | };
58 | }, [getHeadings, visibleHeadings]);
59 |
60 | const scroll = (id: string) => {
61 | for (const heading of Array.from(document.querySelectorAll("h1, h2, h3"))) {
62 | heading.setAttribute("data-highlight", "false");
63 | }
64 |
65 | const element = document.getElementById(id);
66 |
67 | if (element) {
68 | const top = element.offsetTop - 100;
69 | window.scrollTo({
70 | top: top,
71 | behavior: "smooth",
72 | });
73 |
74 | element.setAttribute("data-highlight", "true");
75 |
76 | setTimeout(() => {
77 | element.setAttribute("data-highlight", "false");
78 | }, 2000);
79 | }
80 | };
81 |
82 | return (
83 |
84 |
95 |
96 | {headings.map((heading) => (
97 |
98 | scroll(heading.id)}
101 | className={cn({
102 | "mt-0 ml-2 border-l border-l-gray-4 py-1 text-left text-muted opacity-100 transition ease-in-out hover:opacity-50": true,
103 | "text-bold text-gray-12": visibleHeadings.has(heading.id),
104 | "pl-4": heading.level === "h1",
105 | "pl-6": heading.level === "h2",
106 | "pl-7": heading.level === "h3",
107 | "border-l border-l-gray-12": visibleHeadings.has(heading.id),
108 | })}
109 | data-active={visibleHeadings.has(heading.id) ? "true" : "false"}
110 | >
111 | {heading.text}
112 |
113 |
114 | ))}
115 |
116 |
117 |
118 | );
119 | };
120 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | import plugin from "tailwindcss/plugin";
4 |
5 | const config: Config = {
6 | important: true,
7 | content: [
8 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
10 | "./markdown/**/*.{js,ts,jsx,tsx,mdx}",
11 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
12 | "./mdx-components.tsx",
13 | ],
14 | theme: {
15 | extend: {
16 | colors: {
17 | gray: {
18 | 1: "var(--gray-1)",
19 | 2: "var(--gray-2)",
20 | 3: "var(--gray-3)",
21 | 4: "var(--gray-4)",
22 | 5: "var(--gray-5)",
23 | 6: "var(--gray-6)",
24 | 7: "var(--gray-7)",
25 | 8: "var(--gray-8)",
26 | 9: "var(--gray-9)",
27 | 10: "var(--gray-10)",
28 | 11: "var(--gray-11)",
29 | 12: "var(--gray-12)",
30 | a1: "var(--gray-a1)",
31 | a2: "var(--gray-a2)",
32 | a3: "var(--gray-a3)",
33 | a4: "var(--gray-a4)",
34 | a5: "var(--gray-a5)",
35 | a6: "var(--gray-a6)",
36 | a7: "var(--gray-a7)",
37 | a8: "var(--gray-a8)",
38 | a9: "var(--gray-a9)",
39 | a10: "var(--gray-a10)",
40 | a11: "var(--gray-a11)",
41 | a12: "var(--gray-a12)",
42 | },
43 | black: {
44 | a1: "var(--black-a1)",
45 | a2: "var(--black-a2)",
46 | a3: "var(--black-a3)",
47 | a4: "var(--black-a4)",
48 | a5: "var(--black-a5)",
49 | a6: "var(--black-a6)",
50 | a7: "var(--black-a7)",
51 | a8: "var(--black-a8)",
52 | a9: "var(--black-a9)",
53 | a10: "var(--black-a10)",
54 | a11: "var(--black-a11)",
55 | a12: "var(--black-a12)",
56 | },
57 | white: {
58 | a1: "var(--white-a1)",
59 | a2: "var(--white-a2)",
60 | a3: "var(--white-a3)",
61 | a4: "var(--white-a4)",
62 | a5: "var(--white-a5)",
63 | a6: "var(--white-a6)",
64 | a7: "var(--white-a7)",
65 | a8: "var(--white-a8)",
66 | a9: "var(--white-a9)",
67 | a10: "var(--white-a10)",
68 | a11: "var(--white-a11)",
69 | a12: "var(--white-a12)",
70 | },
71 | pink: {
72 | 1: "var(--pink-1)",
73 | 2: "var(--pink-2)",
74 | 3: "var(--pink-3)",
75 | 4: "var(--pink-4)",
76 | 5: "var(--pink-5)",
77 | 6: "var(--pink-6)",
78 | 7: "var(--pink-7)",
79 | 8: "var(--pink-8)",
80 | 9: "var(--pink-9)",
81 | 10: "var(--pink-10)",
82 | 11: "var(--pink-11)",
83 | 12: "var(--pink-12)",
84 | a1: "var(--pink-a1)",
85 | a2: "var(--pink-a2)",
86 | a3: "var(--pink-a3)",
87 | a4: "var(--pink-a4)",
88 | a5: "var(--pink-a5)",
89 | a6: "var(--pink-a6)",
90 | a7: "var(--pink-a7)",
91 | a8: "var(--pink-a8)",
92 | a9: "var(--pink-a9)",
93 | a10: "var(--pink-a10)",
94 | a11: "var(--pink-a11)",
95 | a12: "var(--pink-a12)",
96 | },
97 | yellow: {
98 | 1: "var(--yellow-1)",
99 | 2: "var(--yellow-2)",
100 | 3: "var(--yellow-3)",
101 | 4: "var(--yellow-4)",
102 | 5: "var(--yellow-5)",
103 | 6: "var(--yellow-6)",
104 | 7: "var(--yellow-7)",
105 | 8: "var(--yellow-8)",
106 | 9: "var(--yellow-9)",
107 | 10: "var(--yellow-10)",
108 | 11: "var(--yellow-11)",
109 | 12: "var(--yellow-12)",
110 | a1: "var(--yellow-a1)",
111 | a2: "var(--yellow-a2)",
112 | a3: "var(--yellow-a3)",
113 | a4: "var(--yellow-a4)",
114 | a5: "var(--yellow-a5)",
115 | a6: "var(--yellow-a6)",
116 | a7: "var(--yellow-a7)",
117 | a8: "var(--yellow-a8)",
118 | a9: "var(--yellow-a9)",
119 | a10: "var(--yellow-a10)",
120 | a11: "var(--yellow-a11)",
121 | a12: "var(--yellow-a12)",
122 | },
123 | background: "var(--bg)",
124 | foreground: "var(--fg)",
125 | muted: "var(--muted)",
126 | hover: "var(--hover)",
127 | border: "var(--border)",
128 | scrollbar: {
129 | thumb: "var(--scrollbar-thumb)",
130 | track: "var(--scrollbar-track)",
131 | },
132 | selection: {
133 | background: "var(--selection-background)",
134 | foreground: "var(--selection-foreground)",
135 | },
136 | highlight: {
137 | background: "var(--highlight-background)",
138 | foreground: "var(--highlight-foreground)",
139 | },
140 | kbd: {
141 | background: "var(--kbd-background)",
142 | foreground: "var(--kbd-foreground)",
143 | border: "var(--kbd-border)",
144 | },
145 | },
146 | fontFamily: {
147 | inter: ["var(--font-inter)"],
148 | apple: ["var(--font-apple)"],
149 | },
150 | borderRadius: {
151 | small: "var(--radius-small)",
152 | base: "var(--radius-base)",
153 | large: "var(--radius-large)",
154 | },
155 | },
156 | },
157 | plugins: [
158 | plugin(({ addUtilities }) => {
159 | addUtilities({
160 | ".text-small": {
161 | fontSize: "12px",
162 | letterSpacing: "0.01px",
163 | },
164 | ".text-default": {
165 | fontSize: "14px",
166 | lineHeight: "21px",
167 | letterSpacing: "-0.09px",
168 | },
169 | });
170 | }),
171 | ],
172 | darkMode: "class",
173 | };
174 |
175 | export default config;
176 |
--------------------------------------------------------------------------------
/app/(posts)/guides/posts/basic-writing-and-formatting-syntax.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Basic Writing and Formatting Syntax"
3 | time:
4 | created: "2024-09-18T19:24:46.167Z"
5 | updated: "2024-10-17T09:56:06.854Z"
6 | ---
7 |
8 | > This guide is an abstraction from Github's Formatting Syntax documentation.[^1]\
9 | > We are using it to show the various styles that we have available.
10 |
11 | # Headings
12 |
13 | To create a heading, add one to two # symbols before your heading text. The number of # you use will determine the hierarchy level and typeface size of the heading.
14 |
15 |
16 |
17 |
Heading 1
18 |
19 |
Heading 2
20 |
21 |
22 |
23 | ```text copy
24 | # Heading 1
25 | # Heading 2
26 | ```
27 |
28 | # Styling Text
29 |
30 | You can indicate emphasis with bold, italic, strikethrough, subscript, or superscript text in comment fields and .md files.
31 |
32 |
33 | **Bold**
34 | *Italic*
35 | ~~Strikethrough~~
36 | Subscript
37 | Superscript
38 | ⌘ K
39 |
40 |
41 | ```text
42 | **Bold**
43 | *Italic*
44 | ~~Strikethrough~~
45 | Subscript
46 | Superscript
47 | ⌘ K
48 | ```
49 |
50 | # Quoting Text
51 |
52 | You can quote text with a > .
53 |
54 |
55 | Text that is not a quote
56 | > Text that is a quote
57 |
58 | ```text
59 | Text that is not a quote
60 | > Text that is a quote
61 | ```
62 |
63 | # Links
64 |
65 | You can create an inline link by wrapping link text in brackets, and then wrapping the URL in parentheses.
66 |
67 |
68 | [Link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
69 |
70 | ```text
71 | [Link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
72 | ```
73 |
74 | # Lists
75 |
76 | You can make an unordered list by preceding one or more lines of text with - .
77 |
78 |
79 | - George Washington
80 | - John Adams
81 | - Thomas Jefferson
82 |
83 | ```text
84 | - George Washington
85 | - John Adams
86 | - Thomas Jefferson
87 | ```
88 | To order your list, precede each line with a number.
89 |
90 |
91 | 1. James Madison
92 | 2. James Monroe
93 | 3. John Quincy Adams
94 |
95 | ```text
96 | 1. James Madison
97 | 2. James Monroe
98 | 3. John Quincy Adams
99 | ```
100 |
101 | # Footnotes
102 |
103 | You can add footnotes to your content by using this bracket syntax `[^1]` and then defining the footnote at the bottom of your content.
104 |
105 |
106 |
107 | The invention of the printing press revolutionized the spread of information in Europe during the 15th century.[^2]
108 |
109 | [^2]: Johannes Gutenberg is credited with inventing the movable-type printing press around 1440 in Mainz, Germany.
110 |
111 | ```text
112 | The invention of the printing press revolutionized the spread of information in Europe during the 15th century.[^2].
113 |
114 | [^2]: Johannes Gutenberg is credited with inventing the movable-type printing press around 1440 in Mainz, Germany.
115 | ```
116 |
117 | # Preview
118 |
119 | You can preview your markdown content in real-time by using the preview feature in the editor. We have been using it throughout this guide to show you how your markdown content will look when rendered. It comes with a single prop, codeblock , that you can use to display the code block right below the preview.
120 |
121 |
122 |
123 |
124 |
125 | ```tsx
126 |
127 | ```
128 |
129 | ```tsx
130 |
131 |
132 |
133 | ```
134 |
135 | # Inline Code
136 |
137 | Append `{:lang}{:js}` (e.g. `{:js}{:js}` ) to the end of inline code to highlight it like a regular code block.
138 |
139 |
140 | To useEffect or not to `useEffect(){:tsx}`, that is the question.
141 |
142 | ```text
143 | This is an array `[1, 2, 3]{:js}` of numbers 1 through 3.
144 | ```
145 |
146 |
147 | # Code Blocks
148 |
149 | To format code or text into its own distinct block, use triple backticks.
150 |
151 |
152 | ```tsx
153 | function feature() {
154 | return "It works in my environment.";
155 | }
156 | ```
157 |
158 | ```text
159 | ```tsx
160 | function feature() {
161 | return "It works in my environment.";
162 | }
163 | ```
164 | ```
165 |
166 | # Media
167 |
168 | You can embed images, videos, and other media in your markdown content. You can also add captions to your images via the ` ` component.
169 |
170 |
171 |
172 |
173 |
174 |
175 | ```text
176 |
177 | ```
178 |
179 | # Tables
180 |
181 | You can create tables by assembling a list of words and dividing them with hyphens - (for the first row), and then separating each column with a pipe | .
182 |
183 |
184 | | First Header | Second Header |
185 | | ------------- | ------------- |
186 | | Content Cell | Content Cell |
187 | | Content Cell | Content Cell |
188 |
189 | ```text
190 | | First Header | Second Header |
191 | | ------------- | ------------- |
192 | | Content Cell | Content Cell |
193 | | Content Cell | Content Cell |
194 | ```
195 |
196 | [^1]: Github, ["Basic Writing and Formatting Syntax"](https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax) Github, 2024. [Accessed: 19-Sep-2024].
--------------------------------------------------------------------------------
/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import type { MDXComponents } from "mdx/types";
2 | import type { MDXRemoteProps } from "next-mdx-remote/rsc";
3 | import type { PluggableList } from "unified";
4 |
5 | import FootnoteBackReference from "@/components/footnote/back-reference";
6 | import FootnoteForwardReference from "@/components/footnote/forward-reference";
7 | import MDXImage from "@/components/image";
8 | import Link from "@/components/link";
9 | import Preview from "@/components/preview";
10 | import { cn } from "@/lib/cn";
11 |
12 | import { MDXRemote } from "next-mdx-remote/rsc";
13 | import React from "react";
14 | import rehypePrettyCode from "rehype-pretty-code";
15 | import rehypeSlug from "rehype-slug";
16 | import remarkGfm from "remark-gfm";
17 |
18 | const components: MDXComponents = {
19 | PreviewExample: () => {
20 | return (
21 |
30 | );
31 | },
32 | Preview: ({ children, codeblock }) => {children} ,
33 | Image: ({ caption, alt, ...props }) => ,
34 | h2: ({ children, id }: React.HTMLAttributes) => {
35 | if (id?.includes("footnote-label")) {
36 | return null;
37 | }
38 | return {children} ;
39 | },
40 | a: ({ children, href }) => {
41 | if (href?.startsWith("#user-content-fn-")) {
42 | return {children} ;
43 | }
44 | return (
45 |
46 | {children}
47 |
48 | );
49 | },
50 | blockquote: ({ className, ...props }: React.HTMLAttributes) => (
51 |
52 | ),
53 | table: ({ className, ...props }: React.HTMLAttributes) => (
54 |
57 | ),
58 | th: ({ className, ...props }: React.HTMLAttributes) => (
59 |
60 | ),
61 | td: ({ className, ...props }: React.HTMLAttributes) => (
62 |
63 | ),
64 | ol: ({ className, ...props }: React.HTMLAttributes) => {
65 | if (
66 | React.Children.toArray(props.children).some(
67 | (child) => React.isValidElement(child) && (child as React.ReactElement).props.id?.includes("user-content-fn-"),
68 | )
69 | ) {
70 | return (
71 |
72 | Footnotes
73 | {props.children}
74 |
75 | );
76 | }
77 | return ;
78 | },
79 | ul: ({ className, ...props }: React.HTMLAttributes) => ,
80 | li: ({ className, children, ...props }: React.HTMLAttributes) => {
81 | if (props.id?.includes("user-content-fn-")) {
82 | return (
83 |
84 | {React.Children.map(children, (child) => {
85 | if (React.isValidElement(child)) {
86 | if (child.type === "p") {
87 | const href = child.props.children.find((child: React.ReactNode) => {
88 | if (React.isValidElement(child)) {
89 | return React.isValidElement(child) && "props" in child && (child.props as { href?: string }).href?.includes("user-content-fnref-");
90 | }
91 | return false;
92 | })?.props.href;
93 |
94 | const filtered = child.props.children.filter((child: React.ReactNode) => {
95 | if (React.isValidElement(child)) {
96 | return !(React.isValidElement(child) && "props" in child && (child.props as { href?: string }).href?.includes("user-content-fnref-"));
97 | }
98 | return true;
99 | });
100 |
101 | return {filtered} ;
102 | }
103 | return child;
104 | }
105 | return child;
106 | })}
107 |
108 | );
109 | }
110 | return {children} ;
111 | },
112 | };
113 |
114 | export function useMDXComponents(components: MDXComponents): MDXComponents {
115 | return {
116 | ...components,
117 | };
118 | }
119 |
120 | export function MDX(props: JSX.IntrinsicAttributes & MDXRemoteProps) {
121 | return (
122 |
145 | );
146 | }
147 |
--------------------------------------------------------------------------------
/app/(posts)/guides/posts/project-structure.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Project Structure"
3 | author:
4 | name: "Raphael Salaja"
5 | link: "https://twitter.com/raphaelsalaja"
6 | handle: "raphaelsalaja"
7 | time:
8 | created: "2024-09-30T14:11:11.816Z"
9 | updated: "2024-10-17T09:56:06.855Z"
10 | ---
11 |
12 | # Ease
13 |
14 | This project's structure is designed to make it extremely easy to add and manage content. We utilize Next.js route groups and MDX to organize our content, along with frontmatter for metadata.
15 |
16 | # Writing
17 |
18 | We organize our content using route groups. This means that when you create a new folder in the posts directory, it automatically creates a new route group for your content.
19 |
20 | # Adding a New Category
21 |
22 | To add a new category:
23 |
24 | 1. Duplicate the guides Folder: Copy the existing guides folder inside the posts directory.
25 | 2. Rename the Folder: Rename the duplicated folder to your desired category name.
26 | 3. Add Content: Inside your new category folder, add your content files (usually .mdx files).
27 |
28 | This structure allows you to expand the site's content effortlessly without manual route configurations.
29 |
30 | For more information on routing in Next.js, refer to the [Next.js Routing Documentation](https://nextjs.org/docs/app/building-your-application/routing).
31 |
32 | # Components
33 |
34 | We use MDX to write our content, which allows us to include React components directly within our markdown files.
35 |
36 | # Adding a New Component
37 |
38 | To add a new component:
39 |
40 | 1. Create the Component: Define your React component as you normally would.
41 | 2. Register the Component: Add your component to the `mdx-components.tsx` file in the root of the project. This file maps component names to their implementations, making them available in your MDX files.
42 |
43 | For more details on using MDX with Next.js, see the [Next.js MDX Documentation](https://nextjs.org/docs/app/building-your-application/configuring/mdx#add-an-mdx-componentstsx-file).
44 |
45 | # Frontmatter
46 |
47 | We use frontmatter to add metadata to our markdown files. This metadata is crucial for categorization, and providing additional context about each post. We've defined an interface for the frontmatter in the `types` directory called `Post`.
48 |
49 | # The Post Interface
50 |
51 | Here's the `Post` interface for reference:
52 |
53 | ```typescript
54 | export type Post = {
55 | title: string;
56 | slug: string;
57 | content: string;
58 | tags?: string[];
59 | summary?: string;
60 |
61 | author?: {
62 | name?: string;
63 | link?: string;
64 | handle?: string;
65 | };
66 |
67 | time: {
68 | created: string;
69 | updated: string;
70 | };
71 |
72 | media?: {
73 | image?: string;
74 | video?: string;
75 | audio?: string;
76 | };
77 |
78 | categorization?: {
79 | readingTime?: string;
80 | };
81 |
82 | seo?: {
83 | title?: string;
84 | description?: string;
85 | keywords?: string[];
86 | };
87 |
88 | audience?: {
89 | likes?: number;
90 | views?: number;
91 | comments?: number;
92 | };
93 |
94 | related?: {
95 | media?: string[];
96 | links?: string[];
97 | posts?: string[];
98 | };
99 |
100 | social?: {
101 | twitter?: string;
102 | facebook?: string;
103 | linkedin?: string;
104 | instagram?: string;
105 | youtube?: string;
106 | pinterest?: string;
107 | others?: string[];
108 | };
109 | };
110 | ```
111 |
112 | # Using Frontmatter in Your Posts
113 |
114 | At the top of your .mdx files, include frontmatter enclosed within --- delimiters. Here's an example:
115 |
116 | ```yaml
117 | ---
118 | title: "Understanding Frontmatter in MDX"
119 | slug: "understanding-frontmatter"
120 | tags: ["MDX", "Frontmatter", "Next.js"]
121 | summary: "A brief guide on using frontmatter in MDX with Next.js."
122 |
123 | author:
124 | name: "Your Name"
125 | link: "https://yourwebsite.com"
126 | handle: "yourhandle"
127 |
128 | time:
129 | created: "2024-10-03T14:00:00.000Z"
130 | updated: "2024-10-03T14:00:00.000Z"
131 |
132 | media:
133 | image: "/images/cover.png"
134 |
135 | seo:
136 | title: "Understanding Frontmatter in MDX"
137 | description: "Learn how to use frontmatter in your MDX files with Next.js."
138 | keywords: ["MDX", "Next.js", "Frontmatter", "Guide"]
139 | ---
140 | ```
141 |
142 | # Example of a Complete Frontmatter
143 |
144 | ```yaml
145 | ---
146 | title: "How to Use Frontmatter Effectively"
147 | slug: "frontmatter-usage"
148 | tags: ["Frontmatter", "MDX", "Tutorial"]
149 | summary: "Learn the ins and outs of using frontmatter in your MDX files."
150 |
151 | author:
152 | name: "Jane Doe"
153 | link: "https://janedoe.com"
154 | handle: "janedoe"
155 |
156 | time:
157 | created: "2024-09-30T14:11:11.816Z"
158 | updated: "2024-10-03T20:13:13.811Z"
159 |
160 | media:
161 | image: "/images/frontmatter-guide.png"
162 |
163 | categorization:
164 | readingTime: "5 min"
165 |
166 | seo:
167 | title: "Effective Frontmatter Usage in MDX"
168 | description: "A comprehensive guide to using frontmatter in MDX for better content management."
169 | keywords: ["MDX", "Frontmatter", "Content Management"]
170 |
171 | audience:
172 | likes: 120
173 | views: 2500
174 | comments: 15
175 |
176 | related:
177 | media: ["/images/related1.png", "/images/related2.png"]
178 | links: ["https://externalresource.com"]
179 | posts: ["another-post", "yet-another-post"]
180 |
181 | social:
182 | twitter: "https://twitter.com/janedoe"
183 | linkedin: "https://linkedin.com/in/janedoe"
184 | others: ["https://mastodon.social/@janedoe"]
185 | ---
186 | ```
187 |
188 | # Automating MDX Timestamp Updates
189 |
190 | To streamline the management of timestamps in your MDX files, we have implemented a script called update-mdx-timestamps.js.
191 | This script runs automatically during the next build process and ensures that the created and updated timestamps in your MDX frontmatter are accurate and up-to-date.
192 | The script is designed to run automatically during the build process, but you can also run it manually if needed.
193 |
194 | # Running Manually
195 |
196 | ```bash
197 | node scripts/update-mdx-timestamps.js
198 | ```
199 |
200 | # Overriding the Created Timestamp
201 |
202 | By default, the script does not override the created timestamp if it already exists. To force the script to update the created timestamp for all files, use the --override-created flag:
203 |
204 | ```bash
205 | node scripts/update-mdx-timestamps.js --override-created
206 | ```
207 |
208 | # Integration with Build Process
209 |
210 | To ensure the script runs automatically, you can add it to the scripts section of your package.json:
211 |
212 | ```json
213 | {
214 | "scripts": {
215 | "build": "node scripts/update-mdx-timestamps.js && next build"
216 | }
217 | }
218 | ```
219 |
220 | This way, every time you run npm run build, the script will execute before the Next.js build process begins.
221 |
222 |
223 |
224 | # Conclusion
225 |
226 | This project structure, combined with the use of MDX and frontmatter, provides a powerful and flexible way to manage content in your Next.js application.
227 | Whether you're adding new categories, integrating custom components, or enhancing your posts with rich metadata, this setup is designed to scale with your needs.
--------------------------------------------------------------------------------
/styles/main.css:
--------------------------------------------------------------------------------
1 | @import url("@radix-ui/colors/gray.css");
2 | @import url("@radix-ui/colors/gray-alpha.css");
3 | @import url("@radix-ui/colors/gray-dark.css");
4 | @import url("@radix-ui/colors/gray-dark-alpha.css");
5 | @import url("@radix-ui/colors/pink.css");
6 | @import url("@radix-ui/colors/pink-alpha.css");
7 | @import url("@radix-ui/colors/pink-dark.css");
8 | @import url("@radix-ui/colors/pink-dark-alpha.css");
9 | @import url("@radix-ui/colors/yellow.css");
10 | @import url("@radix-ui/colors/yellow-alpha.css");
11 | @import url("@radix-ui/colors/yellow-dark.css");
12 | @import url("@radix-ui/colors/yellow-dark-alpha.css");
13 | @import url("@radix-ui/colors/black-alpha.css");
14 | @import url("@radix-ui/colors/white-alpha.css");
15 |
16 | @tailwind base;
17 | @tailwind components;
18 | @tailwind utilities;
19 |
20 | @layer base {
21 | /*
22 | * We are using the Radix UI Colors package to generate a set of color variables that can be used in the project.
23 | * @see: https://colors.radix-ui.com/
24 | */
25 |
26 | :root {
27 | --bg: var(--gray-1);
28 | --fg: var(--gray-12);
29 | --muted: var(--gray-8);
30 | --border: var(--gray-4);
31 | --scrollbar-thumb: var(--gray-4);
32 | --scrollbar-track: transparent;
33 | --selection-background: var(--pink-3);
34 | --selection-foreground: var(--pink-11);
35 | --kbd-background: var(--gray-3);
36 | --kbd-foreground: var(--gray-11);
37 | --kbd-border: var(--gray-4);
38 | --highlight-background: var(--yellow-3);
39 | --highlight-foreground: var(--yellow-11);
40 | --radius-small: 4px;
41 | --radius-base: 8px;
42 | --radius-large: 12px;
43 | }
44 |
45 | * {
46 | --webkit-font-smoothing: antialiased;
47 | --moz-osx-font-smoothing: grayscale;
48 |
49 | font-variant-ligatures: common-ligatures;
50 | text-wrap: pretty;
51 | text-rendering: optimizelegibility;
52 | }
53 |
54 | *::selection {
55 | color: var(--selection-foreground);
56 | background-color: var(--selection-background);
57 | }
58 |
59 | html {
60 | scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
61 | scrollbar-gutter: stable;
62 | scrollbar-width: thin;
63 | scroll-behavior: smooth;
64 | }
65 |
66 | html {
67 | @apply text-default text-foreground bg-background;
68 |
69 | h1,
70 | h2,
71 | h3,
72 | h4,
73 | h5,
74 | h6 {
75 | @apply font-medium;
76 | }
77 |
78 | h2,
79 | h3,
80 | sub,
81 | sup {
82 | @apply text-muted;
83 | }
84 |
85 | sub {
86 | @apply text-small;
87 | }
88 |
89 | a {
90 | @apply transition hover:opacity-50;
91 | }
92 |
93 | ol {
94 | @apply list-decimal;
95 | }
96 |
97 | ul {
98 | @apply list-disc;
99 | }
100 |
101 | kbd {
102 | @apply mx-1;
103 |
104 | display: inline-block;
105 | min-width: 20px;
106 | height: 20px;
107 | min-height: 20px;
108 | padding: 0 6px;
109 | font-size: 12px;
110 | line-height: 20px;
111 | text-align: center;
112 | background: var(--kbd-background);
113 | border-radius: 4px;
114 | box-shadow: 0 0 0 1px var(--kbd-border);
115 | }
116 | }
117 |
118 | article {
119 | h1 + h2 {
120 | margin-top: 4px !important;
121 | }
122 |
123 | h1 + p,
124 | h2 + p {
125 | margin-top: 8px !important;
126 | }
127 |
128 | figure + h1,
129 | p + h1 {
130 | margin-top: 48px !important;
131 | }
132 |
133 | p:not(:first-child),
134 | h1:not(:first-child),
135 | h2:not(:first-child),
136 | h3:not(:first-child),
137 | h4:not(:first-child),
138 | h5:not(:first-child),
139 | h6:not(:first-child),
140 | blockquote:not(:first-child),
141 | ul:not(:first-child),
142 | ol:not(:first-child),
143 | pre:not(:first-child),
144 | figure:not(:first-child) {
145 | margin-top: 24px;
146 | }
147 |
148 | /*
149 | * This handles our highlighting on selection of a table of contents item.
150 | */
151 |
152 | [data-highlight] {
153 | @apply relative;
154 | }
155 |
156 | [data-highlight]::before {
157 | @apply absolute inset-0 -inset-x-1 -z-10 bg-transparent transition scale-y-[1.1] content-[""] duration-500;
158 | }
159 |
160 | [data-highlight="true"]::before {
161 | @apply bg-highlight-background text-highlight-foreground scale-y-110 !important;
162 | }
163 |
164 | [data-highlight="false"]::before {
165 | @apply bg-transparent scale-y-100;
166 | }
167 |
168 | /*
169 | * Here we are setting up the styles for our footnotes and references.
170 | */
171 |
172 | .footnotes {
173 | position: relative;
174 | padding-top: 16px;
175 | margin-top: 64px !important;
176 | border-top: 1px solid var(--border);
177 | }
178 |
179 | .footnotes ol {
180 | position: relative;
181 | display: block;
182 | padding: 0 0 0 4px;
183 | margin-top: 16px !important;
184 | list-style: none;
185 | }
186 |
187 | .footnotes li {
188 | margin-top: 0 !important;
189 | }
190 |
191 | .footnotes ol li {
192 | position: relative;
193 | padding-top: 4px;
194 | padding-left: 6px;
195 | font-size: 12px;
196 | color: var(--muted);
197 | counter-increment: list;
198 | }
199 |
200 | .footnotes ol li::before {
201 | position: absolute;
202 | top: 0;
203 | right: 100%;
204 | font-size: 8px;
205 | content: counter(list);
206 | }
207 |
208 | .footnotes ol li p a {
209 | z-index: 2;
210 | }
211 | }
212 |
213 | /*
214 | * We are using rehypye-pretty to style the code blocks in the project.
215 | * rehype-pretty-code is a Rehype plugin powered by the shiki syntax highlighter that provides beautiful code blocks for Markdown or MDX.
216 | * It works on both the server at build-time (avoiding runtime syntax highlighting) and on the client for dynamic highlighting.
217 | * @see: https://rehype-pretty.pages.dev/
218 | */
219 |
220 | /*
221 | * Here we are setting up the styles needed to enable theme-aware code blocks.
222 | * @see: https://rehype-pretty.pages.dev/#multiple-themes-dark-and-light-mode
223 | */
224 |
225 | html {
226 | code[data-theme*=" "],
227 | code[data-theme*=" "] span {
228 | color: var(--shiki-light);
229 | background-color: var(--shiki-light-bg);
230 | }
231 |
232 | &.dark {
233 | code[data-theme*=" "],
234 | code[data-theme*=" "] span {
235 | color: var(--shiki-dark);
236 | background-color: var(--shiki-dark-bg);
237 | }
238 | }
239 | }
240 |
241 | /*
242 | * Here we are setting up the styles needed to enable line numbers in code blocks.
243 | * @see: https://rehype-pretty.pages.dev/#line-numbers
244 | */
245 |
246 | html {
247 | code[data-line-numbers] {
248 | counter-reset: line;
249 | }
250 |
251 | code[data-line-numbers] > [data-line]::before {
252 | display: inline-block;
253 | width: 12px;
254 | margin-right: 2rem;
255 | color: gray;
256 | text-align: right;
257 | content: counter(line);
258 | counter-increment: line;
259 | }
260 |
261 | code[data-line-numbers-max-digits="2"] > [data-line]::before {
262 | width: 1.25rem;
263 | }
264 |
265 | code[data-line-numbers-max-digits="3"] > [data-line]::before {
266 | width: 1.75rem;
267 | }
268 |
269 | code[data-line-numbers-max-digits="4"] > [data-line]::before {
270 | width: 2.25rem;
271 | }
272 |
273 | p [data-line] {
274 | padding: 2px 4px;
275 | margin-right: 2px;
276 | margin-left: 2px;
277 | text-align: center;
278 | background-color: var(--kbd-background) !important;
279 | border-radius: 4px;
280 | box-shadow: 0 0 0 1px var(--kbd-border);
281 | }
282 | }
283 |
284 | /*
285 | * Here we are just some additional styles to make the code blocks look better.
286 | * @see: https://rehype-pretty.pages.dev/#styles
287 | */
288 |
289 | * {
290 | pre {
291 | padding: 16px;
292 | overflow-x: auto;
293 | font-size: 12px;
294 |
295 | [data-line] {
296 | margin-top: 0 !important;
297 | }
298 | }
299 |
300 | figure {
301 | width: 100%;
302 | margin-top: 4px;
303 | border: 1px solid var(--border);
304 | border-radius: var(--radius-base);
305 | }
306 | }
307 | }
308 |
--------------------------------------------------------------------------------