Add GitHub-style callout support

Implement proper GitHub-style callouts with beautiful styling:

Features:
- Custom rehype plugin to transform > [!NOTE] syntax
- Support for 5 callout types:
  * NOTE (blue, 📝)
  * TIP (green, 💡)
  * IMPORTANT (purple, )
  * WARNING (orange, ⚠️)
  * CAUTION (red, 🚨)
- Gradient backgrounds with accent colors
- Full dark mode support
- Converts existing emoji callouts to proper format

Files:
- lib/rehype-callouts.ts: Custom plugin for parsing
- contentlayer.config.ts: Add plugin to pipeline
- styles/globals.css: Beautiful styling for all types
- content/: Convert 2 emoji callouts to [!TIP] format

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 18:11:29 +08:00
parent f994301fbb
commit af40ebc5e6
6 changed files with 250 additions and 1 deletions

Submodule content updated: c105c2a142...a859f93327

View File

@@ -4,6 +4,7 @@ import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import remarkGfm from 'remark-gfm';
import rehypePrettyCode from 'rehype-pretty-code';
import { rehypeCallouts } from './lib/rehype-callouts';
export const Post = defineDocumentType(() => ({
name: 'Post',
@@ -97,6 +98,7 @@ export default makeSource({
keepBackground: false,
},
],
rehypeCallouts,
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
/**

81
lib/rehype-callouts.ts Normal file
View File

@@ -0,0 +1,81 @@
import { visit } from 'unist-util-visit';
/**
* Rehype plugin to transform GitHub-style blockquote alerts
* Transforms: > [!NOTE] into styled callout boxes
*/
export function rehypeCallouts() {
return (tree: any) => {
visit(tree, 'element', (node, index, parent) => {
// Only process blockquotes
if (node.tagName !== 'blockquote') return;
// Check if first child is a paragraph
if (!node.children || node.children.length === 0) return;
const firstChild = node.children[0];
if (firstChild.tagName !== 'p') return;
// Check if paragraph starts with [!TYPE]
if (!firstChild.children || firstChild.children.length === 0) return;
const firstText = firstChild.children[0];
if (firstText.type !== 'text') return;
const match = firstText.value.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i);
if (!match) return;
const type = match[0].replace(/^\[!|\]\s*/g, '').toLowerCase();
// Remove the [!TYPE] marker from the text
firstText.value = firstText.value.replace(match[0], '');
// If the text node is now empty, remove it
if (!firstText.value.trim()) {
firstChild.children.shift();
}
// Add callout data attributes and classes
node.properties = node.properties || {};
node.properties.className = ['callout', `callout-${type}`];
node.properties['data-callout'] = type;
// Add icon element at the beginning
const iconMap: Record<string, string> = {
note: '📝',
tip: '💡',
important: '❗',
warning: '⚠️',
caution: '🚨',
};
const icon = {
type: 'element',
tagName: 'div',
properties: { className: ['callout-icon'] },
children: [{ type: 'text', value: iconMap[type] || '📝' }],
};
const title = {
type: 'element',
tagName: 'div',
properties: { className: ['callout-title'] },
children: [{ type: 'text', value: type.toUpperCase() }],
};
const header = {
type: 'element',
tagName: 'div',
properties: { className: ['callout-header'] },
children: [icon, title],
};
const content = {
type: 'element',
tagName: 'div',
properties: { className: ['callout-content'] },
children: node.children,
};
node.children = [header, content];
});
};
}

57
package-lock.json generated
View File

@@ -28,6 +28,7 @@
"rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0",
"remark-directive": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^3.15.0",
"tailwind-merge": "^3.4.0",
@@ -7355,6 +7356,27 @@
"node": ">= 0.4"
}
},
"node_modules/mdast-util-directive": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz",
"integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"devlop": "^1.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0",
"parse-entities": "^4.0.0",
"stringify-entities": "^4.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-find-and-replace": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
@@ -7804,6 +7826,25 @@
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-extension-directive": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz",
"integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-factory-whitespace": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0",
"parse-entities": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-frontmatter": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz",
@@ -9712,6 +9753,22 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-directive": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz",
"integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-directive": "^3.0.0",
"micromark-extension-directive": "^4.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-frontmatter": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz",

View File

@@ -35,6 +35,7 @@
"rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0",
"remark-directive": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^3.15.0",
"tailwind-merge": "^3.4.0",

View File

@@ -426,3 +426,111 @@ body {
.prose [data-rehype-pretty-code-title] + pre {
@apply mt-0 rounded-t-none;
}
/* GitHub-style Callouts/Alerts */
.prose .callout {
@apply my-6 rounded-lg border-l-4 p-4 shadow-sm;
background: linear-gradient(135deg, var(--callout-bg-start), var(--callout-bg-end));
}
.prose .callout-header {
@apply mb-3 flex items-center gap-2;
}
.prose .callout-icon {
@apply text-2xl;
line-height: 1;
}
.prose .callout-title {
@apply text-sm font-bold uppercase tracking-wider;
color: var(--callout-title-color);
letter-spacing: 0.05em;
}
.prose .callout-content {
@apply text-sm leading-relaxed;
}
.prose .callout-content > *:first-child {
@apply mt-0;
}
.prose .callout-content > *:last-child {
@apply mb-0;
}
/* NOTE - Blue */
.prose .callout-note {
--callout-bg-start: rgba(59, 130, 246, 0.08);
--callout-bg-end: rgba(59, 130, 246, 0.04);
--callout-title-color: #2563eb;
@apply border-blue-500;
}
.dark .prose .callout-note {
--callout-bg-start: rgba(96, 165, 250, 0.12);
--callout-bg-end: rgba(96, 165, 250, 0.06);
--callout-title-color: #93c5fd;
@apply border-blue-400;
}
/* TIP - Green */
.prose .callout-tip {
--callout-bg-start: rgba(34, 197, 94, 0.08);
--callout-bg-end: rgba(34, 197, 94, 0.04);
--callout-title-color: #16a34a;
@apply border-green-500;
}
.dark .prose .callout-tip {
--callout-bg-start: rgba(74, 222, 128, 0.12);
--callout-bg-end: rgba(74, 222, 128, 0.06);
--callout-title-color: #86efac;
@apply border-green-400;
}
/* IMPORTANT - Purple */
.prose .callout-important {
--callout-bg-start: rgba(168, 85, 247, 0.08);
--callout-bg-end: rgba(168, 85, 247, 0.04);
--callout-title-color: #9333ea;
@apply border-purple-500;
}
.dark .prose .callout-important {
--callout-bg-start: rgba(192, 132, 252, 0.12);
--callout-bg-end: rgba(192, 132, 252, 0.06);
--callout-title-color: #c084fc;
@apply border-purple-400;
}
/* WARNING - Orange/Yellow */
.prose .callout-warning {
--callout-bg-start: rgba(251, 191, 36, 0.08);
--callout-bg-end: rgba(251, 191, 36, 0.04);
--callout-title-color: #d97706;
@apply border-yellow-500;
}
.dark .prose .callout-warning {
--callout-bg-start: rgba(253, 224, 71, 0.12);
--callout-bg-end: rgba(253, 224, 71, 0.06);
--callout-title-color: #fde047;
@apply border-yellow-400;
}
/* CAUTION - Red */
.prose .callout-caution {
--callout-bg-start: rgba(239, 68, 68, 0.08);
--callout-bg-end: rgba(239, 68, 68, 0.04);
--callout-title-color: #dc2626;
@apply border-red-500;
}
.dark .prose .callout-caution {
--callout-bg-start: rgba(248, 113, 113, 0.12);
--callout-bg-end: rgba(248, 113, 113, 0.06);
--callout-title-color: #fca5a5;
@apply border-red-400;
}