@TODO: See NOTES.md, this file is split between here and there. Consider using Nightwatch instead of Playwright for e2e testing.
@TODO: See here for utility components like shortcodes: https://docs.astro.build/en/reference/api-reference/#astroslotsrender
The project includes custom components that can be used in MDX files for enhanced content:
Use the <Signup>
component to add a newsletter signup form to your MDX content:
<Signup
title="Stay Updated"
description="Get the latest articles and insights delivered to your inbox."
buttonText="Subscribe"
placeholder="Enter your email"
/>
Props:
title
(optional): Heading text for the signup form (default: "Newsletter Signup")description
(optional): Description text below the title (default: "Subscribe to get the latest updates and insights.")buttonText
(optional): Text for the submit button (default: "Subscribe")placeholder
(optional): Placeholder text for the email input (default: "Enter your email address")className
(optional): Additional CSS classes to apply
Use the <X>
component to embed tweets in your MDX content:
<X
id="1234567890123456789"
author="username"
content="This is the tweet content..."
date="2024-01-15"
avatar="/path/to/avatar.jpg"
/>
Props:
id
(required): The tweet ID from the Twitter/X URLauthor
(optional): Twitter username (default: "Twitter User")content
(optional): Tweet text content (default: "Loading tweet...")date
(optional): Tweet date in YYYY-MM-DD format (default: current date)avatar
(optional): URL to user's avatar image (default: "/assets/images/default-avatar.png")className
(optional): Additional CSS classes to apply
Note: To use these components in MDX files, make sure to import them at the top of your MDX file:
---
# Your frontmatter here
---
import Signup from '../components/Newsletter/Signup.astro';
import X from '../components/Tweet/index.astro';
# Your Content
<Signup title="Join Our Newsletter" />
<X id="1234567890123456789" author="example" content="Check out this amazing post!" />
The project uses a modern CSS custom properties-based theme system that supports light and dark themes, with extensibility for additional themes like seasonal variations.
The theme system consists of two main files that must be kept in sync:
src/styles/themes.css
- Pure CSS file containing all theme definitions using CSS custom propertiessrc/lib/themes.ts
- TypeScript registry defining available themes for the theme picker component
To add a new theme (e.g., a holiday theme), follow these steps:
Add a new CSS rule with your theme's custom properties:
/* Holiday Theme Example */
[data-theme="holiday"] {
/* Background Colors */
--color-bg: #0f172a;
--color-bg-offset: #1e293b;
/* Text Colors */
--color-text: #f1f5f9;
--color-text-offset: #cbd5e1;
/* Primary Brand Colors */
--color-primary: #dc2626;
--color-primary-offset: #991b1b;
--color-primary-bg: #7f1d1d;
--color-primary-bg-hover: #991b1b;
--color-primary-hover: #b91c1c;
/* Secondary Colors */
--color-secondary: #16a34a;
--color-secondary-offset: #15803d;
--color-secondary-bg: #052e16;
/* Status Colors */
--color-success: #16a34a;
--color-success-offset: #22c55e;
--color-success-bg: #052e16;
--color-info: #0891b2;
--color-info-bg: #164e63;
--color-warning: #a16207;
--color-warning-offset: #ca8a04;
--color-warning-bg: #451a03;
--color-danger: #dc2626;
--color-danger-bg: #7f1d1d;
/* Special Colors */
--color-twitter: #1da1f2;
--color-modal-background: #0f172a;
/* Accent Colors */
--color-accent: #fbbf24;
--color-accent-bg: #451a03;
/* Syntax Highlighting */
--shiki-theme: 'github-dark';
}
Add your theme to the themes array:
export const themes = [
{
id: 'default',
name: 'Light',
description: 'Clean light theme with blue accents',
category: 'core'
},
{
id: 'dark',
name: 'Dark',
description: 'Dark theme with navy background',
category: 'core'
},
{
id: 'holiday',
name: 'Holiday',
description: 'Festive red and green theme for the holidays',
category: 'seasonal',
seasonal: true
}
];
Each theme object supports these properties:
id
(required): Unique identifier used in thedata-theme
attributename
(required): Display name shown in the theme pickerdescription
(optional): Tooltip description for the themecategory
(optional): Used for grouping themes ('core', 'seasonal', etc.)seasonal
(optional): Boolean flag marking temporary/seasonal themes
The theme system provides these CSS custom properties:
--color-bg
/--color-bg-offset
- Background colors--color-text
/--color-text-offset
- Text colors--color-border
- Border color
--color-primary
/--color-primary-offset
- Primary brand colors--color-secondary
/--color-secondary-offset
- Secondary colors
--color-success
/--color-danger
/--color-warning
/--color-info
- Status colors- Background variants available with
-bg
suffix
--color-accent
- Purple accent color for highlights--color-twitter
- Twitter brand color--color-modal-background
- Modal overlay background--shiki-theme
- Syntax highlighting theme name
Themes are applied by setting the data-theme
attribute on the document element:
document.documentElement.setAttribute('data-theme', 'holiday');
The system also respects the user's system preference with @media (prefers-color-scheme: dark)
for users who haven't explicitly chosen a theme.
The project uses a Rehype plugin (src/lib/markdown/rehype-tailwind-classes.ts
) to automatically apply Tailwind CSS classes to rendered Markdown content. This replaces the previous SCSS-based content styling system.
All standard Markdown elements are automatically styled with appropriate Tailwind classes:
- Paragraphs: Proper spacing, readable font size, and line height
- Headings: Typography hierarchy with serif fonts for h1-h3
- Links: Underline effects with hover states
- Images/Videos: Responsive sizing, centering, and shadows
- Lists: Proper indentation and spacing
- Code: Inline code highlighting and block code formatting
- Tables: Full styling with borders and hover effects
- Blockquotes: Left border accent with serif typography
The Rehype plugin also handles specialized Markdown-it plugins:
For tabbed code blocks created by markdown-it plugins:
<!-- This would be processed by a markdown-it plugin -->
::: code-tabs
@tab JavaScript
```js
console.log('Hello, World!')
@tab TypeScript
const message: string = 'Hello, World!'
console.log(message)
:::
Auto-applied classes: Tab navigation, content panels, active states
For collapsible content sections:
<details>
<summary>Click to expand</summary>
This content will be collapsible with proper styling.
</details>
Auto-applied classes: Cursor pointer, remove default markers, content indentation
For code blocks with filename labels:
```js filename="example.js"
const example = true
```
Auto-applied classes: Filename positioning, background styling, opacity effects
For text selection sharing functionality:
<share-highlight>
Select this text to see sharing options
</share-highlight>
Auto-applied classes: CSS custom properties for theming and interaction states
This system replaces the previous _content.scss
and vendor SCSS files:
src/styles/vendor/_codetab.scss
→ Integrated into Rehype pluginsrc/styles/vendor/_expandable.scss
→ Integrated into Rehype pluginsrc/styles/vendor/_namedCodeBlock.scss
→ Integrated into Rehype pluginsrc/styles/vendor/_shareHighlight.scss
→ Integrated into Rehype plugin
The styling is now applied automatically to all rendered Markdown content without needing to import or reference any CSS classes.
- Make sure a fixed height header doesn't cover title text on page navigation
h2:target {
scroll-margin-top: var(--header-height);
}
:is(h2, h3, h4):target {
scroll-margin-top: var(--header-height);
}
Also, we can play around with units like ex, or lh for dynamic spacing:
:target {
scroll-margin-top: calc(var(--header-height) + 1lh);
}
This way, we get different offset based on the line-height of the target element which makes our spacing more dynamic.
- Balance text in titles that span multiple lines
h2 {
text-wrap: balance;
}
- Match scroll bar color to page dark/light theme
Usually, websites switch between dark and light themes by assigning a class like dark or light to the body of the document. However, in our case, fixing the scrollbar requires applying the color-scheme property directly to the root element. Can we tackle this with CSS alone? Enter the :has()
selector.
:root:has(body.dark) {
color-scheme: dark;
}
losst.pro has a modal that pops up for fixing mistakes:
Found a mistake in the text? Let me know about that. Highlight the text with the mistake and press Ctrl+Enter.
- astro-auto-import
- astro-navigation
- astro-webfinger (Mastodon)
Icons are managed through the astro-icon
system with SVG files stored in src/icons/
.
Adding a new icon:
- Add the SVG file to
src/icons/
(use kebab-case naming) - Update
src/components/Sprite/sprites.ts
to add the icon name to theSpriteName
union type - Use the icon with the
Sprite
component
Usage:
---
import Sprite from 'components/Sprite.astro'
---
<Sprite name="fileName" class="customClassName"/>
See src/icons/README.md
for detailed icon documentation.
Any .astro
, .md
, or .mdx
file anywhere within the src/pages/
folder automatically became a page on your site.
Astro uses standard HTML anchor elements to navigate between pages (also called routes), with traditional page refreshes.
<a href="/">Home</a>
The <slot>
element in an included layout will render any child elements between the <Layout>
element used in pages
---
const { frontmatter } = Astro.props;
---
<h1>{frontmatter.title}</h1>
<p>Published on: {frontmatter.pubDate.slice(0,10)}</p>
src/pages/blog.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import BlogPost from '../components/BlogPost.astro';
const allPosts = Object.values(import.meta.glob('../pages/posts/*.md', { eager: true }));
const pageTitle = "My Astro Learning Blog"
---
<BaseLayout pageTitle={pageTitle}>
<ul>
{allPosts.map((post) => <BlogPost url={post.url} title={post.frontmatter.title} />)}
</ul>
</BaseLayout>
src/components/BlogPost.astro
---
const { title, url } = Astro.props
---
<li><a href={url}>{title}</a></li>
Use getCollection
instead of a glob. Frontmatter is returned on the data
key.
---
import { getCollection } from "astro:content";
const allPosts = await getCollection("posts");
---
<BlogPost url={`/posts/${post.slug}/`} title={post.data.title} />
Each blog post should have a Frontmatter tag item:
tags: ["blogging"]
src/pages/tags/[tag].astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import BlogPost from '../../components/BlogPost.astro';
export async function getStaticPaths() {
const allPosts = Object.values(import.meta.glob('../posts/*.md', { eager: true }));
const uniqueTags = [...new Set(allPosts.map((post) => post.frontmatter.tags).flat())];
return uniqueTags.map((tag) => {
const filteredPosts = allPosts.filter((post) => post.frontmatter.tags.includes(tag));
return {
params: { tag },
props: { posts: filteredPosts },
};
});
}
const { tag } = Astro.params;
const { posts } = Astro.props;
---
<BaseLayout pageTitle={tag}>
<p>Posts tagged with {tag}</p>
<ul>
{posts.map((post) => <BlogPost url={post.url} title={post.frontmatter.title}/>)}
</ul>
</BaseLayout>
document.activeElement
import '@testing-library/jest-dom';
import 'html-validate/jest';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { BraidTestProvider } from '../../../entries/test';
import { Button, IconSend } from '..';
describe('Button', () => {
it('should render valid html structure', () => {
expect(
renderToStaticMarkup(
<BraidTestProvider>
<Button>Button</Button>
<Button icon={<IconSend />}>Button</Button>
</BraidTestProvider>,
),
).toHTMLValidate({
extends: ['html-validate:recommended'],
});
});
});
clear && TS_NODE_PROJECT="tsconfig.jest.json" yarn jest eleventy/nunjucksAsyncShortcodes/asyncImageHandler/utils.spec.js --projects test/jest/jest.config.node.ts
The project uses a dual testing setup:
- Unit Tests: Vitest for testing individual functions and components
- E2E Tests: Playwright for end-to-end browser testing
Run all tests (unit + e2e):
npm test
Run only unit tests:
npm run test:unit
Run only e2e tests:
npm run test:e2e
Run tests with coverage report:
npm run test:coverage
The project uses Vitest with the v8 coverage provider to generate comprehensive test coverage reports.
Viewing Coverage Reports:
After running npm run test:coverage
, coverage reports are generated in multiple formats:
- Console Output - Summary displayed in terminal
- HTML Report - Interactive browsable report at
./coverage/index.html
- JSON Report - Machine-readable at
./coverage/coverage-final.json
- LCOV Report - Standard format for CI/CD tools at
./coverage/lcov.info
To view the HTML coverage report in your browser:
# After running coverage
open coverage/index.html # macOS
xdg-open coverage/index.html # Linux
start coverage/index.html # Windows
Coverage Report Locations:
- All coverage reports are stored in the
./coverage
directory - The directory is automatically cleaned before each coverage run
- Coverage includes:
src/**/*.{ts,tsx,astro}
andscripts/**/*.ts
- Excludes: test files, type definitions, and test directories
Note: The coverage/
directory should be added to .gitignore
to avoid committing generated reports.
Unit tests are located alongside their source files:
src/**/*.spec.ts
- Component and library testsscripts/**/__tests__/*.spec.ts
- Build script tests
Example test file locations:
src/lib/helpers/formatDate.spec.ts
scripts/build/__tests__/favicon.spec.ts
clear && egrep -rnw './' --exclude-dir=node_modules --exclude-dir=.yarn --exclude-dir=yarn.lock --exclude-dir=public --exclude-dir=.cache -e 'searchString'
npx jest --clearCache
npm install --platform=linux --arch=x64 sharp
document.querySelector('.contact___callout-flex')
document.getElementById('header__theme-icon').getBoundingClientRect().width