Our (hesitantly) first content-based component
This new version of astro-erudite, v1.5.0, introduces our first content-based component: the callout! I was a bit hesitant about adding this component because, frankly, the entire philosophy behind this project was to be as minimalistic as possible—I wanted to simply provide boilerplate to remove the “busy work” factor that often takes away from the writing process.
However, just based on some blog posts I’ve seen that use this template, I felt like there would be a universal desire just to have this component around in the case where it’d be needed. The primary inspiration came when user @rezaarezvan sent in a PR to add their site, rezaarezvan.com, to the examples section in the README. They had years upon years of accumulated notes and resources on their site, most of which were in the form of LaTeX-style academic content that requires “blocks” for things like definitions, theorems, and proofs. I sent in an encouraging reply to the PR and then started building the component:
[@jktrn] your blog posts are literally insane btw @rezaarezvan how have you written multiple textbooks worth of educational resources???
i think i might add latex-style theorem/lemma/corollary/def/proof/eg/ex/remark blocks to astro-erudite so i can accommodate for these academia-style blogs, like e.g. for exercises it’d just be a component with an expandable section to hide the answer
How does it work?
I’ve added a simple Callout.astro
to src/components
that now comes shipped with the template. It’s a very easy-to-read component that has an insanely long configuration scheme for all of the different types of callouts that I’ve added. Fundamentally, it follows the same paradigm as shadcn/ui which uses class-variance-authority to create different “variants” on top of a base styling scheme:
---import { cn } from '@/lib/utils'import { Icon } from 'astro-icon/components'import { cva, type VariantProps } from 'class-variance-authority'
const calloutConfig = { note: { style: 'border-blue-500 dark:bg-blue-950/5', textColor: 'text-blue-700 dark:text-blue-300', icon: 'lucide:info', },115 collapsed lines
tip: { style: 'border-green-500 dark:bg-green-950/5', textColor: 'text-green-700 dark:text-green-300', icon: 'lucide:lightbulb', }, warning: { style: 'border-amber-500 dark:bg-amber-950/5', textColor: 'text-amber-700 dark:text-amber-300', icon: 'lucide:alert-triangle', }, danger: { style: 'border-red-500 dark:bg-red-950/5', textColor: 'text-red-700 dark:text-red-300', icon: 'lucide:shield-alert', }, important: { style: 'border-purple-500 dark:bg-purple-950/5', textColor: 'text-purple-700 dark:text-purple-300', icon: 'lucide:message-square-warning', }, definition: { style: 'border-teal-600 dark:bg-teal-950/10', textColor: 'text-teal-700 dark:text-teal-400', icon: 'lucide:book-open', }, axiom: { style: 'border-teal-500 dark:bg-teal-950/10', textColor: 'text-teal-600 dark:text-teal-300', icon: 'lucide:anchor', }, notation: { style: 'border-teal-300 dark:bg-teal-950/10', textColor: 'text-teal-500 dark:text-teal-200', icon: 'lucide:pen-tool', }, theorem: { style: 'border-indigo-500 dark:bg-indigo-950/10', textColor: 'text-indigo-700 dark:text-indigo-400', icon: 'lucide:check-circle', }, lemma: { style: 'border-indigo-400 dark:bg-indigo-950/10', textColor: 'text-indigo-600 dark:text-indigo-300', icon: 'lucide:puzzle', }, corollary: { style: 'border-indigo-300 dark:bg-indigo-950/10', textColor: 'text-indigo-500 dark:text-indigo-200', icon: 'lucide:git-branch', }, proposition: { style: 'border-indigo-300 dark:bg-indigo-950/10', textColor: 'text-indigo-500 dark:text-indigo-200', icon: 'lucide:file-text', }, conjecture: { style: 'border-fuchsia-500 dark:bg-fuchsia-950/10', textColor: 'text-fuchsia-700 dark:text-fuchsia-300', icon: 'lucide:help-circle', }, proof: { style: 'border-amber-500 dark:bg-amber-950/10', textColor: 'text-amber-700 dark:text-amber-300', icon: 'lucide:check-square', }, remark: { style: 'border-sky-300 dark:bg-sky-950/10', textColor: 'text-sky-500 dark:text-sky-200', icon: 'lucide:message-circle', }, intuition: { style: 'border-sky-300 dark:bg-sky-950/10', textColor: 'text-sky-500 dark:text-sky-200', icon: 'lucide:lightbulb', }, recall: { style: 'border-sky-300 dark:bg-sky-950/10', textColor: 'text-sky-500 dark:text-sky-200', icon: 'lucide:rotate-ccw', }, example: { style: 'border-sky-500 dark:bg-sky-950/10', textColor: 'text-sky-600 dark:text-sky-300', icon: 'lucide:code', }, explanation: { style: 'border-sky-500 dark:bg-sky-950/10', textColor: 'text-sky-600 dark:text-sky-300', icon: 'lucide:help-circle', }, exercise: { style: 'border-pink-600 dark:bg-pink-950/5', textColor: 'text-pink-700 dark:text-pink-300', icon: 'lucide:dumbbell', }, problem: { style: 'border-pink-600 dark:bg-pink-950/5', textColor: 'text-pink-700 dark:text-pink-300', icon: 'lucide:alert-circle', }, answer: { style: 'border-pink-300 dark:bg-pink-950/5', textColor: 'text-pink-500 dark:text-pink-200', icon: 'lucide:check', }, solution: { style: 'border-pink-300 dark:bg-pink-950/5', textColor: 'text-pink-500 dark:text-pink-200', icon: 'lucide:check-circle-2', }, summary: { style: 'border-emerald-600 dark:bg-emerald-950/10', textColor: 'text-emerald-700 dark:text-emerald-300', icon: 'lucide:list', },} as const
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
const calloutVariants = cva('relative px-4 py-3 my-6 border-l-4 text-sm', { variants: { variant: Object.fromEntries( Object.entries(calloutConfig).map(([key, config]) => [key, config.style]), ), }, defaultVariants: { variant: 'note', },})
As such, if you feel like there’s any variant that you’re missing, it’s insanely trivial to add it yourself. It’s less trivial, however, to figure out what colors to use since I’ve essentially taken up every good Tailwind color for these!
How do I use these?
Within any src/content/blog/**/*.mdx
file, you can now use the Callout
component by importing it like so underneath your frontmatter:
---title: 'v1.5.0: “A Callout Component for Nerds”'description: 'A quick update introduces our first content-based component: the callout!'date: 2025-04-24tags: ['v1.5.0']image: './1200x630.png'authors: ['enscribe']---
import Callout from '@/components/Callout.astro'
Then, you can use the component like so. This is just an example but you should actually read the text since it’s relevant to the article:
<Callout title="About Github-flavored alerts" variant="important"> I know that Github typically uses the following syntax for "alerts" (which is what they call these callouts, you can see their documentation [here](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts)):9 collapsed lines
```md showLineNumbers=false > [!NOTE] > Useful information that users should know, even when skimming content. ``` The above syntax is supposed to render like this: <Callout> Useful information that users should know, even when skimming content. </Callout> I believe they do this so they can keep you within the Markdown-like syntax system. However, since this is MDX, I thought we'd be better off just using the component paradigm since it's a bit impractical to make a rehype plugin that can parse this form of syntax (also, solutions for this already exist, such as [lin-stephanie/rehype-callouts](https://github.com/lin-stephanie/rehype-callouts)). Additionally, I find it a pain to work with the `>{:md}` (the `<blockquote>{:html}` indicators for Markdown) syntax, as it makes it difficult to nest things such as code blocks within them in pure Markdown. We'll use components for now!</Callout>
Important (About Github-flavored alerts)
I know that Github typically uses the following syntax for “alerts” (which is what they call these callouts, you can see their documentation here):
> [!NOTE]> Useful information that users should know, even when skimming content.
The above syntax is supposed to render like this:
Note
Useful information that users should know, even when skimming content.
I believe they do this so they can keep you within the Markdown-like syntax system. However, since this is MDX, I thought we’d be better off just using the component paradigm since it’s a bit impractical to make a rehype plugin that can parse this form of syntax (also, solutions for this already exist, such as lin-stephanie/rehype-callouts). Additionally, I find it a pain to work with the >
(the <blockquote>
indicators for Markdown) syntax, as it makes it difficult to nest things such as code blocks within them in pure Markdown. We’ll use components for now!
Callout only supports three props:
Prop | Description | Default |
---|---|---|
title | The title of the callout | undefined |
variant | The variant of the callout | "note" |
defaultOpen | Whether the callout <details> box is open by default | true |
I’ve added an insane amount of variants to this component for potentially any use case you could think of. For the more general ones, you can use the following:
Variant | Usage |
---|---|
Note | For general information or comments that don’t fit other categories |
Tip | For helpful advice or shortcuts related to the topic at hand |
Warning | For potential pitfalls or common misconceptions |
Danger | For things that could potentially be destructive or harmful |
Important | For things that are important to the reader’s understanding |
For the potentially more academic/mathy folk, you can use the following. I’ve created specific variants that are tailor-made to be used alongside and/or nested into each other:
Variant | Usage |
---|---|
Definition | For defining terms or concepts |
Axiom | For fundamental assumptions that are accepted without proof |
Notation | For explaining mathematical notation |
Theorem | For important mathematical or logical statements that have been proven |
Lemma | For helper theorems used in proving larger results |
Corollary | For results that follow directly from theorems |
Proposition | For important statements that are less significant than theorems |
Conjecture | For unproven statements believed to be true |
Proof | For logical arguments that establish the truth of a theorem or lemma |
Remark | For additional comments or (potentially out-of-scope) observations |
Intuition | For explaining the intuitive reasoning behind concepts |
Recall | For reminding readers of previously covered material |
Example | For illustrating concepts with concrete examples or analogies |
Explanation | For providing deeper insights or clarifying complex topics |
Exercise | For practice problems or take-home challenges for the reader |
Problem | For presenting problems to be solved thoroughly |
Answer | For providing simple, short answers to exercises or problems |
Solution | For detailed solutions to exercises or problems |
Summary | For summarizing key points or concepts |
Generic callouts
These are what the generic callouts look like (of course, all the text is made up):
Note (Prerequisites for advanced React development)
This tutorial assumes you’re familiar with React hooks and the component lifecycle. If you need a refresher, check out the official React documentation before proceeding.
Tip (Productivity enhancement)
You can quickly format your code by pressing Ctrl + Alt + F (Windows/Linux) or Cmd + Option + F (Mac).
Additional shortcuts include:
Action | Windows/Linux | Mac |
---|---|---|
Search | Ctrl + F | Cmd + F |
Replace | Ctrl + H | Cmd + H |
Save all | Ctrl + Alt + S | Cmd + Option + S |
Warning (Cross-browser compatibility issues)
This API is not supported in Internet Explorer and has limited support in older browsers. Make sure to include appropriate polyfills.
if (!Object.fromEntries) { Object.fromEntries = function(entries) { const obj = {}; for (const [key, value] of entries) { obj[key] = value; } return obj; };}
See the Browser Compatibility Table for detailed information.
Danger (Critical data loss risk)
Running this command will permanently delete all files in your current directory. Make sure to back up important data before proceeding.
# This will delete everything in the current directoryrm -rf ./*# Safer alternative with confirmationrm -ri ./*
This operation cannot be undone and recovery tools may not be able to restore your data.
Important (Major API changes in v2.0)
Version 2.0 introduces significant API changes. You’ll need to update your existing code to use the new parameter structure.
- The
configure()
method now returns a Promise - Authentication requires an API key object instead of a string
- Event handlers use a new callback pattern
A migration example looks like the following:
app.configure("api-key-string");await app.configure({ key: "api-key-string", version: "2.0" });
app.on('event', callback);app.on('event', { handler: callback, options: { once: true } });
Mathematical callouts
Like I mentioned before, some of these variants are meant to be nested within each other. Take, for example, the following:
Definition (Continuous function)
A function between topological spaces is continuous if for every open set , the preimage is open in .
Explanation (Equivalent formulations)
For metric spaces, continuity can be characterized by: such that . This captures the intuition that points close to each other in map to points close to each other in .
Theorem (Law of Large Numbers)
Let be a sequence of independent and identically distributed random variables with expected value . Then for any :
Proof (Proof)
We’ll prove this using Chebyshev’s inequality. Let and . By Chebyshev’s inequality:
Since the variables are independent, we have:
Substituting this into our inequality:
As , the right side approaches 0, which proves the theorem.
Lemma (Monotone Convergence Theorem)
Let be a sequence of non-negative measurable functions such that for all and almost all . Define . Then:
Proof (Proof)
Let where . By Fatou’s lemma:
For the reverse inequality, note that for all , so . Taking the limit:
Combining these inequalities:
Therefore, .
I’ll add this little subgenre of variant called “example-based” callouts that have more generally a question-answer format. For the “exercise-answer” pairing, the difference is that I’d recommend you default the answer to a closed state to hide the answer from any peeping readers until they’ve actually done the work themselves. Typically, the difference between an “Answer” and a “Solution” is that the answer basically just gives you the final answer, while the solution will show you the steps it takes to get to the answer:
Exercise (Finding the derivative of a product function)
Calculate the derivative of using the product rule.
Answer
Problem (Convergence of arithmetic means)
Prove that if a sequence converges to , then the sequence of arithmetic means also converges to .
Solution (Detailed proof)
Let be given. Since converges to , there exists such that for all . Let be the sequence of arithmetic means.
We can split as follows:
For , we have:
Let . Then:
As , and . So for sufficiently large , we have:
Therefore, the sequence of arithmetic means converges to .