{blog.author}
-- @{blog.author} -
+ ++ {blog.title} +
+ + {/* Tags */} + {blog.tags && ( ++
-
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..e69de29
diff --git a/.env.example b/.env.example
deleted file mode 100644
index 5359c0a..0000000
--- a/.env.example
+++ /dev/null
@@ -1 +0,0 @@
-NEXT_PUBLIC_SITE_URL=site.url
\ No newline at end of file
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..e69de29
diff --git a/.eslintrc.json b/.eslintrc.json
index 4f757ae..0655483 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,8 +1,3 @@
{
- "extends": [
- "next/core-web-vitals",
- "standard",
- "plugin:tailwindcss/recommended",
- "prettier"
- ]
+ "extends": ["next/core-web-vitals", "standard", "plugin:tailwindcss/recommended", "prettier"]
}
diff --git a/.github/ISSUE_TEMPLATE/another.md b/.github/ISSUE_TEMPLATE/another.md
new file mode 100644
index 0000000..8b1f50c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/another.md
@@ -0,0 +1,19 @@
+---
+name: ➕ Other Request
+about: Use this template for any other type of request or remark
+title: "[Other] "
+labels: other
+assignees: abdelkabirouadoukou
+---
+
+## Description
+
+Describe your request or remark.
+
+## Context
+
+Add any relevant context or examples.
+
+## Additional Information
+
+Add links, screenshots, or details if needed.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..6297940
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: 🐞 Bug Report
+about: Report a bug or issue
+title: "[Bug] "
+labels: bug
+assignees: abdelkabirouadoukou
+---
+
+## Description
+
+Describe the bug you encountered.
+
+## Steps to Reproduce
+
+List the steps to reproduce the bug:
+
+1. ...
+2. ...
+3. ...
+
+## Expected Behavior
+
+Describe what you expected to happen.
+
+## Screenshots / Logs
+
+Add screenshots or logs if possible.
+
+## Additional Information
+
+- Project version:
+- Operating system:
+- Browser:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..d1ad858
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,23 @@
+---
+name: 🚀 Feature Request
+about: Propose a new feature for the project
+title: "[Feature] "
+labels: enhancement, Feature Request
+assignees: abdelkabirouadoukou
+---
+
+## Description
+
+Describe the requested feature and its purpose.
+
+## Motivation
+
+Why is this feature important?
+
+## Proposed Solution
+
+Explain how it could be implemented.
+
+## Additional Information
+
+Add any relevant context or examples.
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 0000000..febb5dc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,19 @@
+---
+name: ❓ Question
+about: Ask a question about the project
+title: "[Question] "
+labels: question
+assignees: abdelkabirouadoukou
+---
+
+## Question
+
+Describe your question or request for help.
+
+## Context
+
+Add any relevant context or examples.
+
+## Additional Information
+
+Add links or details if needed.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..427c5e3
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,22 @@
+# Pull Request
+
+## Description
+
+Briefly describe the changes you made and the motivation behind them.
+
+## Type of Change
+
+- [ ] Bug fix
+- [ ] New feature
+- [ ] Enhancement
+- [ ] Documentation update
+
+## Checklist
+
+- [ ] All tests pass
+- [ ] Documentation is updated
+- [ ] No conflicts with the main branch
+
+## Additional Information
+
+Add any relevant context, screenshots, or notes for reviewers.
diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml
new file mode 100644
index 0000000..2e7573c
--- /dev/null
+++ b/.github/workflows/auto-assign.yml
@@ -0,0 +1,22 @@
+name: Auto Assign
+
+on:
+ issues:
+ types: [opened]
+ pull_request:
+ types: [opened]
+
+jobs:
+ run:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - name: 'Auto-assign issue'
+ uses: pozil/auto-assign-issue@v1
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ assignees: abdelkabirouadoukou
+ numOfAssignee: 2
+ reviewers: abdelkabirouadoukou
\ No newline at end of file
diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml
new file mode 100644
index 0000000..96aa315
--- /dev/null
+++ b/.github/workflows/code-check.yml
@@ -0,0 +1,30 @@
+name: Code Format Check
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ format-check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install Bun
+ run: |
+ curl -fsSL https://bun.sh/install | bash
+ echo "$HOME/.bun/bin" >> $GITHUB_PATH
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22.x'
+
+ - name: Install dependencies
+ run: bun ci
+
+ - name: Check formatting
+ run: bun run format:check
\ No newline at end of file
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
new file mode 100644
index 0000000..224740c
--- /dev/null
+++ b/.github/workflows/pr-check.yml
@@ -0,0 +1,26 @@
+name: PR Checks
+
+on:
+ pull_request:
+ branches: [main]
+
+jobs:
+ lint-build-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Install Bun
+ run: |
+ curl -fsSL https://bun.sh/install | bash
+ echo "$HOME/.bun/bin" >> $GITHUB_PATH
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22.x'
+ - name: Install dependencies
+ run: bun ci
+ - name: Lint
+ run: bun run lint
+ - name: Build
+ run: bun run build
diff --git a/.gitignore b/.gitignore
index 9928588..4eb83ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,8 +26,10 @@ yarn-debug.log*
yarn-error.log*
# local env files
-.env*.local
-.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
# vercel
.vercel
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..d1bb1c0
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,24 @@
+{
+ "semi": true,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "useTabs": false,
+ "quoteProps": "as-needed",
+ "bracketSpacing": true,
+ "bracketSameLine": false,
+ "arrowParens": "avoid",
+ "endOfLine": "lf",
+ "embeddedLanguageFormatting": "auto",
+ "htmlWhitespaceSensitivity": "css",
+ "insertPragma": false,
+ "jsxSingleQuote": false,
+ "proseWrap": "preserve",
+ "requirePragma": false,
+ "vueIndentScriptAndStyle": false,
+ "experimentalTernaries": false,
+ "plugins": [
+ "prettier-plugin-tailwindcss"
+ ]
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4d17928
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM node:22-alpine
+
+WORKDIR /app
+COPY package.json .
+RUN bun i
+
+COPY . .
+RUN bun run build
+
+CMD ["bun", "run", "start"]
\ No newline at end of file
diff --git a/Dockerfile.dev b/Dockerfile.dev
new file mode 100644
index 0000000..92b9218
--- /dev/null
+++ b/Dockerfile.dev
@@ -0,0 +1,11 @@
+FROM node:22-alpine
+
+WORKDIR /app
+COPY package.json .
+RUN bun i
+
+COPY . .
+
+EXPOSE 3000
+
+CMD ["bun", "run", "dev"]
\ No newline at end of file
diff --git a/bun.lock b/bun.lock
index 37aa447..b288b73 100644
--- a/bun.lock
+++ b/bun.lock
@@ -7,10 +7,12 @@
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "framer-motion": "^12.23.12",
"core-js": "^3.45.1",
"lucide-react": "^0.379.0",
"next": "14.2.3",
"next-sitemap": "^4.2.3",
+ "prettier-plugin-tailwindcss": "^0.6.14",
"react": "^18",
"react-dom": "^18",
"rehype-autolink-headings": "^7.1.0",
@@ -570,6 +572,8 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+ "framer-motion": ["framer-motion@12.23.12", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg=="],
+
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -868,6 +872,10 @@
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+ "motion-dom": ["motion-dom@12.23.12", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw=="],
+
+ "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
@@ -964,6 +972,8 @@
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
+ "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
+
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..7f77941
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,31 @@
+version: '3.8'
+
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "3000:3000"
+ env_file:
+ - .env.production.local
+ restart: unless-stopped
+ container_name: thinktapfast-blog-prod
+
+ app-dev:
+ build:
+ context: .
+ dockerfile: Dockerfile.dev
+ ports:
+ - "3001:3000"
+ env_file:
+ - .env.development.local
+ volumes:
+ - .:/app
+ - /app/node_modules
+ - /app/.next
+ environment:
+ - NODE_ENV=development
+ - WATCHPACK_POLLING=true
+ restart: unless-stopped
+ container_name: thinktapfast-blog-dev
\ No newline at end of file
diff --git a/next-sitemap.config.js b/next-sitemap.config.js
index 48218ef..cbb6643 100644
--- a/next-sitemap.config.js
+++ b/next-sitemap.config.js
@@ -1,9 +1,9 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
- siteUrl: 'https://thinktapfast-blog.vercel.app',
- generateRobotsTxt: true, // generate also robots.txt
- changefreq: 'weekly',
- priority: 0.7,
- sitemapSize: 7000,
- exclude: ['/admin/*', '/api/*'], // exclude URLs
+ siteUrl: "https://thinktapfast-blog.vercel.app",
+ generateRobotsTxt: true, // generate also robots.txt
+ changefreq: "weekly",
+ priority: 0.7,
+ sitemapSize: 7000,
+ exclude: ["/admin/*", "/api/*"], // exclude URLs
};
diff --git a/package.json b/package.json
index 53e72e8..8ba4cb4 100644
--- a/package.json
+++ b/package.json
@@ -5,19 +5,30 @@
"scripts": {
"dev": "next dev",
"build": "next build",
+ "build:sitemap": "next-sitemap",
"start": "next start",
"lint": "next lint",
- "prettier": "pnpx prettier --write .",
- "postbuild": "next-sitemap"
+ "lint:fix": "next lint --fix",
+ "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
+ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
+ "docker:build": "docker build -t thinktapfast-app .",
+ "docker:run": "docker run -p 3000:3000 --env-file .env.production.local thinktapfast-app",
+ "docker:build:dev": "docker build -t thinktapfast-app-dev -f Dockerfile.dev .",
+ "docker:run:dev:cmd": "docker run -p 3000:3000 --env-file .env.development.local -v %cd%:/app -v /app/node_modules thinktapfast-app-dev",
+ "docker:run:dev:powershell": "powershell -ExecutionPolicy Bypass -File scripts/docker-dev.ps1",
+ "docker:run:dev:mac": "bash scripts/docker-dev.sh",
+ "docker:run:dev:linux": "bash scripts/docker-dev.sh"
},
"dependencies": {
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "framer-motion": "^12.23.12",
"core-js": "^3.45.1",
"lucide-react": "^0.379.0",
"next": "14.2.3",
"next-sitemap": "^4.2.3",
+ "prettier-plugin-tailwindcss": "^0.6.14",
"react": "^18",
"react-dom": "^18",
"rehype-autolink-headings": "^7.1.0",
diff --git a/public/images/author/abdelkabir.jpeg b/public/images/author/abdelkabir.jpeg
new file mode 100644
index 0000000..04db12a
Binary files /dev/null and b/public/images/author/abdelkabir.jpeg differ
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/sitemap.xml b/public/sitemap.xml
index ec1d401..39fed00 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -1,4 +1,3 @@
- Web Developer -
+Web Developer
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Sequi, harum - odio! Molestias natus possimus dolorem modi libero eaque in aliquam - harum recusandae nam! Reprehenderit soluta fuga consequuntur, iure - corrupti autem! Lorem ipsum dolor sit amet consectetur adipisicing - elit. Modi asperiores voluptate, veritatis non placeat numquam. - Repellendus mollitia aut reprehenderit est. Reprehenderit soluta fuga - consequuntur, iure corrupti autem! Lorem ipsum dolor sit amet - consectetur adipisicing elit. Modi asperiores voluptate, veritatis non - placeat numquam. Repellendus mollitia aut reprehenderit est. + Lorem ipsum dolor sit amet consectetur adipisicing elit. Sequi, harum odio! Molestias + natus possimus dolorem modi libero eaque in aliquam harum recusandae nam! Reprehenderit + soluta fuga consequuntur, iure corrupti autem! Lorem ipsum dolor sit amet consectetur + adipisicing elit. Modi asperiores voluptate, veritatis non placeat numquam. Repellendus + mollitia aut reprehenderit est. Reprehenderit soluta fuga consequuntur, iure corrupti + autem! Lorem ipsum dolor sit amet consectetur adipisicing elit. Modi asperiores voluptate, + veritatis non placeat numquam. Repellendus mollitia aut reprehenderit est.
Sorry, the blog post you are looking for does not exist.
{blog.author}
-- @{blog.author} -
+ +{publishedBlogs.length} articles published
+{blog.description}
- )} + {publishedBlogs.length ? ( +- {formatDate(blog.date)} -
- )} + return ( +No Blogs found
- )} ++ {blog.description} +
+ )} + + {blog.tags &&{blog.author}
+{formatDate(blog.date)}
+No articles found.
++ {blog.description} +
+ )} + ++ No articles found with the tag "{decodedTag}". +
+ + ← Back to all articles + +- {siteConfig.description} + Explore how ThinkTapFast helps startups, creators, and businesses scale smarter with + AI-powered text, image, and voice content.
{authorData.bio}
+ )} + + {/* Social Links */} + {(authorData.social?.github || authorData.social?.twitter) && ( +
+ {children}
+
+ );
+ },
code: ({ className, ...props }: ComponentsProps) => (
-
- {NAV_LIST.map((item) => (
+
+ {NAV_LIST.map(item => (
{
+const MobileLink = ({ children, onOpenChange, className, href, ...props }: MobileLinkProps) => {
const router = useRouter();
const pathname = usePathname();
return (
diff --git a/src/components/page-header.tsx b/src/components/page-header.tsx
index ef7688d..5d8cfed 100644
--- a/src/components/page-header.tsx
+++ b/src/components/page-header.tsx
@@ -12,9 +12,7 @@ export default function PageHeader({ title, description }: PageHeaderProps) {
{title}
- {description && (
- {description}
- )}
+ {description && {description}
}
);
diff --git a/src/components/related-posts.tsx b/src/components/related-posts.tsx
new file mode 100644
index 0000000..a3bbf46
--- /dev/null
+++ b/src/components/related-posts.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import React from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { formatDate } from "@/lib/utils";
+import { Clock, ArrowRight } from "lucide-react";
+import { getAuthor } from "@/config/authors";
+import type { Blog } from "@/types/globals";
+
+interface RelatedPostsProps {
+ currentPost: Blog;
+ allPosts: Blog[];
+ maxPosts?: number;
+}
+
+export function RelatedPosts({ currentPost, allPosts, maxPosts = 3 }: RelatedPostsProps) {
+ // Find related posts based on author and exclude current post
+ const relatedPosts = React.useMemo(() => {
+ const otherPosts = allPosts.filter(post => post.slug !== currentPost.slug && post.published);
+
+ // Prioritize posts by same author
+ const sameAuthorPosts = otherPosts.filter(post => post.author === currentPost.author);
+
+ // Get other recent posts if we don't have enough from same author
+ const recentPosts = otherPosts
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
+ .slice(0, maxPosts * 2);
+
+ // Combine and deduplicate
+ const combined = [...sameAuthorPosts, ...recentPosts];
+ const unique = combined.filter(
+ (post, index, arr) => arr.findIndex(p => p.slug === post.slug) === index,
+ );
+
+ return unique.slice(0, maxPosts);
+ }, [currentPost, allPosts, maxPosts]);
+
+ if (relatedPosts.length === 0) return null;
+
+ return (
+
+
+ Related Articles
+ Continue reading with these related posts
+
+
+
+ {relatedPosts.map(post => {
+ const author = getAuthor(post.author || "");
+
+ return (
+
+ {post.image && (
+
+
+
+ )}
+
+
+
+ {post.title}
+
+
+ {post.description && (
+
+ {post.description}
+
+ )}
+
+
+
+ {author?.avatar && (
+
+ )}
+ {post.author}
+
+
+
+
+ 5m read
+
+
+
+
+
+
+
+
+
+
+
+ Read {post.title}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index 2cfc0bd..8aefb95 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -2,11 +2,11 @@
import React, { useState } from "react";
import Link from "next/link";
import { AlignLeft, X } from "lucide-react";
-import { siteConfig } from "@/config/site";
+import { appConfig } from "@/config/app.config";
import HeaderNav from "@/components/header-nav";
import { Button } from "@/components/ui/button";
import MobileNav from "@/components/mobile-nav";
-import { Icons } from "./icons";
+// import { Icons } from "./icons";
export default function SiteHeader() {
const [isMobileOpen, setIsMobileOpen] = useState(false);
@@ -15,8 +15,8 @@ export default function SiteHeader() {
-
- {siteConfig.name}
+ {/* */}
+ {appConfig.name}
@@ -27,19 +27,13 @@ export default function SiteHeader() {
onClick={() => setIsMobileOpen(!isMobileOpen)}
>
<>
- {isMobileOpen ? (
-
- ) : (
-
- )}
+ {isMobileOpen ? : }
Menu
>
- {isMobileOpen && (
- setIsMobileOpen(false)} />
- )}
+ {isMobileOpen && setIsMobileOpen(false)} />}
);
}
diff --git a/src/components/table-of-contents.tsx b/src/components/table-of-contents.tsx
new file mode 100644
index 0000000..37c0d03
--- /dev/null
+++ b/src/components/table-of-contents.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { cn } from "@/lib/utils";
+import { List } from "lucide-react";
+
+interface TocItem {
+ id: string;
+ title: string;
+ level: number;
+}
+
+interface TableOfContentsProps {
+ className?: string;
+}
+
+export function TableOfContents({ className }: TableOfContentsProps) {
+ const [toc, setToc] = useState([]);
+ const [activeId, setActiveId] = useState("");
+
+ useEffect(() => {
+ // Extract headings from the content (skip h1, focus on h2-h4)
+ const headings = document.querySelectorAll("h2, h3, h4");
+ const tocItems: TocItem[] = [];
+
+ headings.forEach((heading, index) => {
+ const id = heading.id || `heading-${index}`;
+ if (!heading.id) {
+ heading.id = id;
+ }
+
+ tocItems.push({
+ id,
+ title: heading.textContent || "",
+ level: parseInt(heading.tagName.charAt(1)),
+ });
+ });
+
+ setToc(tocItems);
+
+ // Set up intersection observer for active heading
+ const observer = new IntersectionObserver(
+ entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ setActiveId(entry.target.id);
+ }
+ });
+ },
+ {
+ rootMargin: "-20% 0% -80% 0%",
+ threshold: 0,
+ },
+ );
+
+ headings.forEach(heading => observer.observe(heading));
+
+ return () => observer.disconnect();
+ }, []);
+
+ const scrollToHeading = (id: string) => {
+ const element = document.getElementById(id);
+ if (element) {
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+ }
+ };
+
+ if (toc.length === 0) return null;
+
+ return (
+
+
+
+
+ Contents
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/background-lines.tsx b/src/components/ui/background-lines.tsx
new file mode 100644
index 0000000..ff80e46
--- /dev/null
+++ b/src/components/ui/background-lines.tsx
@@ -0,0 +1,71 @@
+"use client";
+import React from "react";
+import { cn } from "@/lib/utils";
+
+export const BackgroundLines = ({
+ children,
+ className,
+ svgOptions,
+}: {
+ children: React.ReactNode;
+ className?: string;
+ svgOptions?: {
+ duration?: number;
+ };
+}) => {
+ return (
+
+
+ {children}
+
+ );
+};
+
+const SVGGrid = ({
+ svgOptions,
+}: {
+ svgOptions?: {
+ duration?: number;
+ };
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index c552fae..c492390 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -10,12 +10,9 @@ const buttonVariants = cva(
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
- destructive:
- "bg-destructive text-destructive-foreground hover:bg-destructive/90",
- outline:
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
@@ -43,11 +40,7 @@ const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
-
+
);
},
);
diff --git a/src/components/ui/code-block.tsx b/src/components/ui/code-block.tsx
new file mode 100644
index 0000000..f1d51f2
--- /dev/null
+++ b/src/components/ui/code-block.tsx
@@ -0,0 +1,65 @@
+"use client";
+import { useState } from "react";
+import { Check, Copy } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface CodeBlockProps {
+ code: string;
+ language?: string;
+ filename?: string;
+ className?: string;
+}
+
+export function CodeBlock({ code, language = "javascript", filename, className }: CodeBlockProps) {
+ const [copied, setCopied] = useState(false);
+
+ const copyToClipboard = async () => {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {filename || `${language || "text"}`}
+
+
+
+
+ {/* Code Content */}
+
+
+
+ {code}
+
+
+
+ {/* Gradient overlay for long code */}
+
+
+
+ );
+}
diff --git a/src/components/ui/tracing-beam.tsx b/src/components/ui/tracing-beam.tsx
new file mode 100644
index 0000000..76b6f6a
--- /dev/null
+++ b/src/components/ui/tracing-beam.tsx
@@ -0,0 +1,126 @@
+"use client";
+import React, { useEffect, useRef, useState } from "react";
+import { motion, useTransform, useScroll, useSpring } from "framer-motion";
+import { cn } from "@/lib/utils";
+
+export const TracingBeam = ({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) => {
+ const ref = useRef(null);
+ const { scrollYProgress } = useScroll({
+ target: ref,
+ offset: ["start start", "end end"],
+ });
+
+ const contentRef = useRef(null);
+ const [svgHeight, setSvgHeight] = useState(800);
+
+ useEffect(() => {
+ if (contentRef.current) {
+ setSvgHeight(contentRef.current.offsetHeight);
+ }
+
+ const handleResize = () => {
+ if (contentRef.current) {
+ setSvgHeight(contentRef.current.offsetHeight);
+ }
+ };
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, [children]);
+
+ const y1 = useSpring(useTransform(scrollYProgress, [0, 0.7], [50, svgHeight - 100]), {
+ stiffness: 500,
+ damping: 90,
+ });
+ const y2 = useSpring(useTransform(scrollYProgress, [0, 1], [50, svgHeight - 50]), {
+ stiffness: 500,
+ damping: 90,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+};
diff --git a/src/config/app.config.ts b/src/config/app.config.ts
new file mode 100644
index 0000000..1b709b7
--- /dev/null
+++ b/src/config/app.config.ts
@@ -0,0 +1,13 @@
+import authorAvatar from "../../public/images/author/abdelkabir.jpeg";
+export const appConfig = {
+ name: "ThinkTapFast | Blog",
+ description:
+ "MDX Blog Template is a simple implementation of a markdown static blog. Built with Next.js 14 and velite js.",
+ author: "ThinkTapFast Team",
+ authorImage: authorAvatar,
+ social: {
+ github: "https://github.com/ThinkTapFast",
+ },
+};
+
+export type AppConfig = typeof appConfig;
diff --git a/src/config/authors.ts b/src/config/authors.ts
new file mode 100644
index 0000000..1f0f8b1
--- /dev/null
+++ b/src/config/authors.ts
@@ -0,0 +1,41 @@
+// Author profiles for blog posts
+export const authors = {
+ Abdelkabir: {
+ name: "Abdelkabir",
+ username: "abdelkabir",
+ avatar: "/images/author/abdelkabir.jpeg",
+ bio: "Full-stack developer passionate about modern web technologies",
+ social: {
+ github: "https://github.com/abdelkabir",
+ twitter: "https://twitter.com/abdelkabir",
+ },
+ },
+ devbertskie: {
+ name: "Dev Bertskie",
+ username: "devbertskie",
+ avatar: "/images/author/devbertskie.png",
+ bio: "Software engineer and tech enthusiast",
+ social: {
+ github: "https://github.com/devbertskie",
+ twitter: "https://twitter.com/devbertskie",
+ },
+ },
+ "ThinkTapFast Team": {
+ name: "ThinkTapFast Team",
+ username: "thinktapfast",
+ avatar: "/images/author/abdelkabir.jpeg", // Default team avatar
+ bio: "Tech team building innovative solutions",
+ social: {
+ github: "https://github.com/ThinkTapFast",
+ twitter: undefined,
+ },
+ },
+} as const;
+
+export type AuthorKey = keyof typeof authors;
+export type Author = (typeof authors)[AuthorKey];
+
+// Helper function to get author by name
+export function getAuthor(authorName: string): Author | null {
+ return authors[authorName as AuthorKey] || null;
+}
diff --git a/src/config/site.ts b/src/config/site.ts
deleted file mode 100644
index b905af9..0000000
--- a/src/config/site.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import authorAvatar from "../../public/images/author/devbertskie.png";
-export const siteConfig = {
- name: "Mdx Blog Template",
- description:
- "MDX Blog Template is a simple implementation of a markdown static blog. Built with Next.js 14 and velite js.",
- author: "devbertskie",
- authorImage: authorAvatar,
- social: {
- github: "https://github.com/devbertskie",
- twitter: "https://twitter.com",
- facebook: "https://facebook.com",
- },
-};
-
-export type SiteConfig = typeof siteConfig;
\ No newline at end of file
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 86a88d2..350f28d 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -1,5 +1,5 @@
import { Icons } from "@/components/icons";
-import { siteConfig } from "@/config/site";
+import { appConfig } from "@/config/app.config";
import { Bot, Rss } from "lucide-react";
export const NAV_LIST = [
@@ -7,8 +7,4 @@ export const NAV_LIST = [
{ label: "About", path: "/about", icon: Bot },
];
-export const SOCIALS = [
- { label: "Github", path: siteConfig.social.github, icon: Icons.github },
- { label: "Facebook", path: siteConfig.social.facebook, icon: Icons.facebook },
- { label: "Twitter", path: siteConfig.social.twitter, icon: Icons.x },
-];
+export const SOCIALS = [{ label: "Github", path: appConfig.social.github, icon: Icons.github }];
diff --git a/src/content/blog/advanced-blog-features.mdx b/src/content/blog/advanced-blog-features.mdx
new file mode 100644
index 0000000..affe66c
--- /dev/null
+++ b/src/content/blog/advanced-blog-features.mdx
@@ -0,0 +1,144 @@
+---
+title: "[Example] Advanced Next.js Blog Features"
+description: "Explore the enhanced blog layout with sidebar, table of contents, reading progress, and improved UI components"
+image: "/images/blog/extends.webp"
+date: "2025-09-04"
+author: "Abdelkabir"
+tags: ["nextjs", "react", "blog", "ui", "typescript"]
+readTime: 12
+featured: true
+---
+
+# Advanced Next.js Blog Features
+
+Welcome to our enhanced blog experience! This post demonstrates all the new features we've implemented to make reading and navigating our content more enjoyable.
+
+## Enhanced User Interface
+
+Our blog now features a modern, responsive design with several key improvements:
+
+### Sidebar Navigation
+The right sidebar provides quick access to essential features:
+- **Table of Contents** - Navigate directly to any section
+- **Reading Progress** - Track your progress through the article
+- **Quick Actions** - Share, bookmark, and like articles
+- **Article Information** - Reading time and publication details
+
+### Improved Blog Listing
+The blog index page now showcases:
+- **Card-based Layout** - Clean, modern post previews
+- **Author Information** - Profile pictures and social links
+- **Reading Time Estimates** - Know what to expect before diving in
+- **Enhanced Typography** - Better readability and visual hierarchy
+
+## Technical Implementation
+
+### Table of Contents Generation
+Our TOC system automatically:
+1. **Scans Headings** - Extracts all H1-H6 elements from content
+2. **Generates IDs** - Creates clean, URL-friendly anchor links
+3. **Tracks Progress** - Highlights the current section while reading
+4. **Smooth Scrolling** - Provides seamless navigation between sections
+
+### Reading Progress Tracking
+The progress indicator:
+- **Calculates Percentage** - Based on scroll position relative to content
+- **Visual Feedback** - Shows completion status at a glance
+- **Responsive Design** - Adapts to different screen sizes
+
+### Author System
+We've implemented a comprehensive author management system:
+- **Author Profiles** - Detailed information for each contributor
+- **Social Links** - Connect with authors on GitHub and Twitter
+- **Consistent Branding** - Unified presentation across all posts
+
+## Browser Compatibility
+
+Thanks to our core-js integration, all these features work seamlessly across:
+
+### Modern Browsers
+- ✅ Chrome 90+
+- ✅ Firefox 88+
+- ✅ Safari 14+
+- ✅ Edge 90+
+
+### Legacy Support
+- ✅ Internet Explorer 11
+- ✅ Chrome 40+
+- ✅ Firefox 35+
+- ✅ Safari 9+
+
+## Performance Optimizations
+
+### Image Handling
+- **Next.js Image Component** - Automatic optimization and lazy loading
+- **Responsive Images** - Serves appropriate sizes for different devices
+- **Modern Formats** - WebP support with fallbacks
+
+### Code Splitting
+- **Dynamic Imports** - Components load only when needed
+- **Bundle Optimization** - Reduced initial page load times
+- **Progressive Enhancement** - Core functionality works without JavaScript
+
+## Mobile Experience
+
+The enhanced design is fully responsive:
+
+### Touch-Friendly Interface
+- **Large Touch Targets** - Easy navigation on mobile devices
+- **Gesture Support** - Smooth scrolling and interactions
+- **Optimized Typography** - Readable text at all screen sizes
+
+### Mobile-First Design
+- **Progressive Disclosure** - Sidebar collapses on mobile
+- **Optimized Navigation** - Easy access to all features
+- **Fast Loading** - Optimized for mobile networks
+
+## Accessibility Features
+
+We've prioritized accessibility throughout:
+
+### Keyboard Navigation
+- **Tab Order** - Logical navigation flow
+- **Focus Indicators** - Clear visual feedback
+- **Skip Links** - Quick access to main content
+
+### Screen Reader Support
+- **Semantic HTML** - Proper heading hierarchy
+- **ARIA Labels** - Descriptive text for interactive elements
+- **Alt Text** - Comprehensive image descriptions
+
+## Content Management
+
+### MDX Integration
+- **Rich Content** - Combine Markdown with React components
+- **Syntax Highlighting** - Beautiful code blocks
+- **Interactive Elements** - Embed dynamic content
+
+### Metadata Handling
+- **SEO Optimization** - Proper meta tags and structured data
+- **Social Sharing** - Open Graph and Twitter Card support
+- **Automated Sitemap** - Keep search engines updated
+
+## Future Enhancements
+
+We're continuously improving the blog experience:
+
+### Planned Features
+- **Search Functionality** - Find content quickly
+- **Comment System** - Engage with readers
+- **Related Posts** - Discover more content
+- **Tags and Categories** - Better content organization
+
+### Performance Improvements
+- **Service Worker** - Offline reading capability
+- **Prefetching** - Faster navigation between posts
+- **CDN Integration** - Global content delivery
+
+## Conclusion
+
+These enhancements represent a significant step forward in our blogging platform. The combination of modern design, accessibility features, and performance optimizations creates an exceptional reading experience.
+
+Whether you're browsing on a desktop with the full sidebar experience or reading on mobile with our responsive design, every feature is crafted to help you focus on what matters most: the content.
+
+We're excited to continue improving and adding new features based on your feedback. Happy reading!
diff --git a/src/content/blog/core-js-features-demo.mdx b/src/content/blog/core-js-features-demo.mdx
new file mode 100644
index 0000000..c5c126d
--- /dev/null
+++ b/src/content/blog/core-js-features-demo.mdx
@@ -0,0 +1,98 @@
+---
+title: "[Example] Core-js Powers Modern JavaScript Features"
+description: "See how core-js enables modern JavaScript in your blog content, even on older browsers"
+image: "/images/blog/any-considered.webp"
+date: "2025-09-04"
+author: "Abdelkabir"
+tags: ["javascript", "core-js", "polyfills", "browser-compatibility"]
+readTime: 8
+---
+
+# Core-js Features in Action
+
+This blog post demonstrates how **core-js polyfills** enable modern JavaScript features in your MDX content, ensuring compatibility across all browsers.
+
+## Array Methods (Polyfilled by Core-js)
+
+```javascript
+// These methods work in IE11+ thanks to core-js
+const posts = ['intro', 'advanced', 'tutorial'];
+
+// Array.includes (IE11 needs polyfill)
+const hasIntro = posts.includes('intro'); // ✅ Works everywhere
+
+// Array.find (IE11 needs polyfill)
+const foundPost = posts.find(post => post.startsWith('adv')); // ✅ Works everywhere
+
+// Array.findIndex (IE11 needs polyfill)
+const introIndex = posts.findIndex(post => post === 'intro'); // ✅ Works everywhere
+```
+
+## Object Methods (Polyfilled by Core-js)
+
+```javascript
+// Modern object methods work across all browsers
+const blogMeta = { title: 'My Post', date: '2025-09-04' };
+
+// Object.entries (IE11 needs polyfill)
+Object.entries(blogMeta).forEach(([key, value]) => {
+ console.log(`${key}: ${value}`); // ✅ Works everywhere
+});
+
+// Object.values (IE11 needs polyfill)
+const values = Object.values(blogMeta); // ✅ Works everywhere
+
+// Object.assign (IE11 needs polyfill)
+const extendedMeta = Object.assign({}, blogMeta, { author: 'You' }); // ✅ Works everywhere
+```
+
+## String Methods (Polyfilled by Core-js)
+
+```javascript
+const postTitle = "How to use Core-js in your MDX blog";
+
+// String.includes (IE11 needs polyfill)
+const isAboutCoreJs = postTitle.includes('Core-js'); // ✅ Works everywhere
+
+// String.startsWith (IE11 needs polyfill)
+const isHowTo = postTitle.startsWith('How to'); // ✅ Works everywhere
+
+// String.endsWith (IE11 needs polyfill)
+const isAboutBlog = postTitle.endsWith('blog'); // ✅ Works everywhere
+```
+
+## Modern Collections (Polyfilled by Core-js)
+
+```javascript
+// Map and Set work in older browsers
+const tagMap = new Map([
+ ['javascript', 'JavaScript'],
+ ['typescript', 'TypeScript'],
+ ['react', 'React']
+]); // ✅ Works in IE11+
+
+const uniqueTags = new Set(['javascript', 'typescript', 'javascript']); // ✅ Works in IE11+
+```
+
+## Browser Compatibility
+
+Thanks to core-js, this blog supports:
+
+- ✅ **Internet Explorer 11+**
+- ✅ **Chrome 40+**
+- ✅ **Firefox 35+**
+- ✅ **Safari 9+**
+- ✅ **Edge (all versions)**
+- ✅ **Mobile browsers**
+
+## Development Features
+
+In development mode, you can see polyfill activity in the browser console:
+
+```javascript
+// Console output examples:
+console.log('🔧 Core-js polyfills applied for: Array.includes, String.startsWith, Map');
+console.log('✅ Modern browser detected - all features supported natively');
+```
+
+Your MDX content can use modern JavaScript features confidently, knowing core-js provides seamless backward compatibility!
diff --git a/src/content/blog/intro.mdx b/src/content/blog/intro.mdx
deleted file mode 100644
index 2414612..0000000
--- a/src/content/blog/intro.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: "Hello World"
-description: "Discover when Blog"
-image: "/images/blog/any-considered.webp"
-date: "2025-08-03"
-author: "abdelkabir"
----
-hello
\ No newline at end of file
diff --git a/src/content/blog/welcome.mdx b/src/content/blog/welcome.mdx
new file mode 100644
index 0000000..6ee7cac
--- /dev/null
+++ b/src/content/blog/welcome.mdx
@@ -0,0 +1,32 @@
+---
+title: "Welcome"
+description: "Discover when Blog"
+image: "/images/blog/any-considered.webp"
+date: "2025-08-03"
+author: "Abdelkabir"
+tags: ["welcome", "introduction", "blog"]
+readTime: 3
+featured: true
+---
+
+# Welcome to Our Blog!
+
+Hello and welcome to our tech blog! This is where we share insights, tutorials, and thoughts about modern web development.
+
+## What You'll Find Here
+
+- **Modern Web Development**: Latest trends in React, Next.js, and TypeScript
+- **Best Practices**: Code quality, testing, and deployment strategies
+- **Tutorials**: Step-by-step guides to build amazing applications
+- **Personal Insights**: Our experiences and lessons learned
+
+## Getting Started
+
+This blog is built with:
+- **Next.js 14** with App Router
+- **TypeScript** for type safety
+- **Tailwind CSS** for styling
+- **MDX** for rich content
+- **Core-js** for browser compatibility
+
+We hope you find our content helpful and engaging. Happy reading!
\ No newline at end of file
diff --git a/src/styles/globals.css b/src/styles/globals.css
index d85a87b..a4febbf 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -2,6 +2,24 @@
@tailwind components;
@tailwind utilities;
+@layer utilities {
+ .line-clamp-3 {
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .scrollbar-hide {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+
+ .scrollbar-hide::-webkit-scrollbar {
+ display: none;
+ }
+}
+
@layer base {
:root {
--background: 270 100% 95%;
diff --git a/src/styles/mdx.css b/src/styles/mdx.css
index 921faa6..0e3dc77 100644
--- a/src/styles/mdx.css
+++ b/src/styles/mdx.css
@@ -3,7 +3,7 @@
}
[data-rehype-pretty-code-figure] code {
- @apply text-sm !leading-loose md:text-base border-0 p-0;
+ @apply border-0 p-0 text-sm !leading-loose md:text-base;
}
[data-rehype-pretty-code-figure] code[data-line-numbers] {
@@ -35,5 +35,5 @@
}
.subheading-anchor {
- @apply no-underline before:content-["#"] hover:before:content-["#"] -ml-4 before:text-background hover:before:text-primary transition-colors before:mr-1 before:text-lg;
+ @apply -ml-4 no-underline transition-colors before:mr-1 before:text-lg before:text-background before:content-["#"] hover:before:text-primary hover:before:content-["#"];
}
diff --git a/src/types/globals.ts b/src/types/globals.ts
index 49d1a7d..8a3e6a7 100644
--- a/src/types/globals.ts
+++ b/src/types/globals.ts
@@ -8,4 +8,7 @@ export interface Blog {
image?: string;
author: string;
body: string;
+ tags?: string[];
+ readTime?: number;
+ featured?: boolean;
}
diff --git a/tsconfig.json b/tsconfig.json
index d367136..d23961f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -27,7 +27,8 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
- "./.velite"
+ "./.velite",
+ "next-sitemap.config.js"
],
"exclude": ["node_modules"]
}
diff --git a/velite.config.ts b/velite.config.ts
index 824cb2a..18cadcb 100644
--- a/velite.config.ts
+++ b/velite.config.ts
@@ -20,6 +20,9 @@ const blogs = defineCollection({
published: s.boolean().default(true),
image: s.string().max(99),
author: s.string(),
+ tags: s.array(s.string()).optional(),
+ readTime: s.number().optional(),
+ featured: s.boolean().optional(),
body: s.mdx(),
})
.transform(computedFields),