Logo astro-erudite
v1.5.0: “A Callout Component for Nerds”

v1.5.0: “A Callout Component for Nerds”

April 24, 2025
13 min read
Table of Contents
index

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:

src/components/Callout.astro
---
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:

src/content/blog/callouts-component/index.mdx
---
title: 'v1.5.0: “A Callout Component for Nerds”'
description: 'A quick update introduces our first content-based component: the callout!'
date: 2025-04-24
tags: ['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:

PropDescriptionDefault
titleThe title of the calloutundefined
variantThe variant of the callout"note"
defaultOpenWhether the callout <details> box is open by defaulttrue

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:

VariantUsage
NoteFor general information or comments that don’t fit other categories
TipFor helpful advice or shortcuts related to the topic at hand
WarningFor potential pitfalls or common misconceptions
DangerFor things that could potentially be destructive or harmful
ImportantFor 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:

VariantUsage
DefinitionFor defining terms or concepts
AxiomFor fundamental assumptions that are accepted without proof
NotationFor explaining mathematical notation
TheoremFor important mathematical or logical statements that have been proven
LemmaFor helper theorems used in proving larger results
CorollaryFor results that follow directly from theorems
PropositionFor important statements that are less significant than theorems
ConjectureFor unproven statements believed to be true
ProofFor logical arguments that establish the truth of a theorem or lemma
RemarkFor additional comments or (potentially out-of-scope) observations
IntuitionFor explaining the intuitive reasoning behind concepts
RecallFor reminding readers of previously covered material
ExampleFor illustrating concepts with concrete examples or analogies
ExplanationFor providing deeper insights or clarifying complex topics
ExerciseFor practice problems or take-home challenges for the reader
ProblemFor presenting problems to be solved thoroughly
AnswerFor providing simple, short answers to exercises or problems
SolutionFor detailed solutions to exercises or problems
SummaryFor 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:

ActionWindows/LinuxMac
SearchCtrl + FCmd + F
ReplaceCtrl + HCmd + H
Save allCtrl + Alt + SCmd + 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.

polyfill.js
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.

danger.sh
# This will delete everything in the current directory
rm -rf ./*
# Safer alternative with confirmation
rm -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.

  1. The configure() method now returns a Promise
  2. Authentication requires an API key object instead of a string
  3. 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 f:XYf: X \rightarrow Y between topological spaces is continuous if for every open set VYV \subseteq Y, the preimage f1(V)Xf^{-1}(V) \subseteq X is open in XX.

Explanation (Equivalent formulations)

For metric spaces, continuity can be characterized by: ε>0,δ>0\forall \varepsilon > 0, \exists \delta > 0 such that dX(x,y)<δ    dY(f(x),f(y))<εd_X(x,y) < \delta \implies d_Y(f(x),f(y)) < \varepsilon. This captures the intuition that points close to each other in XX map to points close to each other in YY.

Theorem (Law of Large Numbers)

Let X1,X2,X_1, X_2, \ldots be a sequence of independent and identically distributed random variables with expected value E[Xi]=μ<\mathbb{E}[X_i] = \mu < \infty. Then for any ε>0\varepsilon > 0:

limnP(1ni=1nXiμ>ε)=0 \lim_{n \to \infty} P\left(\left|\frac{1}{n}\sum_{i=1}^{n}X_i - \mu\right| > \varepsilon\right) = 0
Proof (Proof)

We’ll prove this using Chebyshev’s inequality. Let Sn=i=1nXiS_n = \sum_{i=1}^{n}X_i and σ2=Var(Xi)\sigma^2 = \text{Var}(X_i). By Chebyshev’s inequality:

P(Snnμε)Var(Sn/n)ε2 P\left(\left|\frac{S_n}{n} - \mu\right| \geq \varepsilon\right) \leq \frac{\text{Var}(S_n/n)}{\varepsilon^2}

Since the variables are independent, we have:

Var(Sn/n)=Var(Sn)n2=nσ2n2=σ2n \text{Var}(S_n/n) = \frac{\text{Var}(S_n)}{n^2} = \frac{n\sigma^2}{n^2} = \frac{\sigma^2}{n}

Substituting this into our inequality:

P(Snnμε)σ2nε2 P\left(\left|\frac{S_n}{n} - \mu\right| \geq \varepsilon\right) \leq \frac{\sigma^2}{n\varepsilon^2}

As nn \to \infty, the right side approaches 0, which proves the theorem.

Lemma (Monotone Convergence Theorem)

Let (fn)(f_n) be a sequence of non-negative measurable functions such that fn(x)fn+1(x)f_n(x) \leq f_{n+1}(x) for all nNn \in \mathbb{N} and almost all xx. Define f(x)=limnfn(x)f(x) = \lim_{n \to \infty} f_n(x). Then:

limnfndμ=fdμ \lim_{n \to \infty} \int f_n \, d\mu = \int f \, d\mu
Proof (Proof)

Let gn=fnχEg_n = f_n \cdot \chi_E where E={x:f(x)<}E = \{x : f(x) < \infty\}. By Fatou’s lemma:

fdμ=limngndμlim infngndμ=lim infnfndμ \int f \, d\mu = \int \lim_{n \to \infty} g_n \, d\mu \leq \liminf_{n \to \infty} \int g_n \, d\mu = \liminf_{n \to \infty} \int f_n \, d\mu

For the reverse inequality, note that fnff_n \leq f for all nn, so fndμfdμ\int f_n \, d\mu \leq \int f \, d\mu. Taking the limit:

lim supnfndμfdμ \limsup_{n \to \infty} \int f_n \, d\mu \leq \int f \, d\mu

Combining these inequalities:

fdμlim infnfndμlim supnfndμfdμ \int f \, d\mu \leq \liminf_{n \to \infty} \int f_n \, d\mu \leq \limsup_{n \to \infty} \int f_n \, d\mu \leq \int f \, d\mu

Therefore, limnfndμ=fdμ\lim_{n \to \infty} \int f_n \, d\mu = \int f \, d\mu.

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 f(x)=x3sin(x)f(x) = x^3 \sin(x) using the product rule.

Answer
ddx[x3sin(x)]=3x2sin(x)+x3cos(x) \frac{d}{dx}[x^3 \sin(x)] = 3x^2 \sin(x) + x^3 \cos(x)
Problem (Convergence of arithmetic means)

Prove that if a sequence (an)(a_n) converges to LL, then the sequence of arithmetic means (a1+a2++ann)(\frac{a_1 + a_2 + \ldots + a_n}{n}) also converges to LL.

Solution (Detailed proof)

Let ε>0\varepsilon > 0 be given. Since (an)(a_n) converges to LL, there exists NNN \in \mathbb{N} such that anL<ε2|a_n - L| < \frac{\varepsilon}{2} for all nNn \geq N. Let Sn=a1+a2++annS_n = \frac{a_1 + a_2 + \ldots + a_n}{n} be the sequence of arithmetic means.

We can split SnS_n as follows:

Sn=a1+a2++aN+aN+1++annS_n = \frac{a_1 + a_2 + \ldots + a_N + a_{N+1} + \ldots + a_n}{n}

For n>Nn > N, we have:

SnL=a1+a2++annL=a1+a2++annLn=(a1L)+(a2L)++(anL)na1L+a2L++aNL+aN+1L++anLn\begin{align*} |S_n - L| &= \left|\frac{a_1 + a_2 + \ldots + a_n}{n} - L\right| \\ &= \left|\frac{a_1 + a_2 + \ldots + a_n - nL}{n}\right| \\ &= \left|\frac{(a_1 - L) + (a_2 - L) + \ldots + (a_n - L)}{n}\right| \\ &\leq \frac{|a_1 - L| + |a_2 - L| + \ldots + |a_N - L| + |a_{N+1} - L| + \ldots + |a_n - L|}{n} \end{align*}

Let M=max{a1L,a2L,,aNL}M = \max\{|a_1 - L|, |a_2 - L|, \ldots, |a_N - L|\}. Then:

SnLNM+(nN)ε2n=NMn+nNnε2 |S_n - L| \leq \frac{NM + (n-N)\frac{\varepsilon}{2}}{n} = \frac{NM}{n} + \frac{n-N}{n} \cdot \frac{\varepsilon}{2}

As nn \to \infty, NMn0\frac{NM}{n} \to 0 and nNn1\frac{n-N}{n} \to 1. So for sufficiently large nn, we have:

SnL<ε |S_n - L| < \varepsilon

Therefore, the sequence of arithmetic means converges to LL.