Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a492321
feat: add Avatar component
g-francesca Mar 4, 2026
7aea4bc
feat: add post cards
g-francesca Mar 4, 2026
9c2f5c5
style: post cards
g-francesca Mar 4, 2026
90e6261
feat: dynamic blog listing page
g-francesca Mar 5, 2026
d1076eb
feat: add Pagination component
g-francesca Mar 5, 2026
0f3c0aa
style: refine listing style
g-francesca Mar 5, 2026
86d9089
feat: add Tag component
g-francesca Mar 5, 2026
230b669
feat: review Avatar component to include multiple authors
g-francesca Mar 5, 2026
fcec8ae
feat: set Tabs to filter posts
g-francesca Mar 5, 2026
3488f08
style: refine pagination and tag
g-francesca Mar 6, 2026
9490956
fix: remove usless index
g-francesca Mar 6, 2026
9c6e1bf
fix: minor style fixes
g-francesca Mar 9, 2026
20743a0
fix: formatting
g-francesca Mar 9, 2026
091e4c0
fix: clean up blog posts tags
g-francesca Mar 10, 2026
60d9957
fix: use Image astro component
g-francesca Mar 10, 2026
761c537
fix: pagination links for accessibility
g-francesca Mar 10, 2026
5c6b897
fix: color contrast for tags
g-francesca Mar 10, 2026
e351421
fix: formatting
g-francesca Mar 10, 2026
00cf88c
fix(theme): disable CSS transitions during theme change to prevent fl…
ShubhamOulkar Mar 10, 2026
2ba4ed6
fix: show authors on blog posts
ShubhamOulkar Mar 10, 2026
c57b72e
style: blog page with 2 cols on tablet
g-francesca Mar 11, 2026
e176b88
Merge branch 'redesign-blog-listing' of github.com:expressjs/expressj…
g-francesca Mar 11, 2026
85051d8
Merge branch 'redesign' of https://github.com/expressjs/expressjs.com…
bjohansebas Mar 12, 2026
91aa5b6
add recent blog
bjohansebas Mar 12, 2026
a15a966
fixup!
bjohansebas Mar 12, 2026
9fbcc8b
fix: sidebar taking the entire heigh when doing screenshots
g-francesca Mar 12, 2026
02b7910
refactor: review listing pagination
g-francesca Mar 12, 2026
0faa05e
fix: always use english posts
g-francesca Mar 12, 2026
d4ec28f
fix: improve theme toggle reliability and fix Firefox/safari flashing
ShubhamOulkar Mar 13, 2026
bde3b0f
fix focus state PostCard component.
ShubhamOulkar Mar 13, 2026
e016880
fix: hover state PostCard component
ShubhamOulkar Mar 13, 2026
9f39652
fix: allow tab focus only on selected Tab to move focus on tabs use l…
ShubhamOulkar Mar 13, 2026
3bae1fc
fix: focus main-content on skip to main content
ShubhamOulkar Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added astro/public/posts/sample-cover.png
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, we’ll have OG images generated automatically, but that will be handled in a separate PR.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions astro/src/components/patterns/PageHead/PageHead.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
import './PageHead.css';
import { Flex, H1, Body } from '@components/primitives';

interface Props {
title: string;
description?: string;
}

const { title, description } = Astro.props;
---

<Flex direction="column" gap="6" align="center" class="page-head">
<H1 vMargin={false}>{title}</H1>
{
description && (
<Body vMargin={false} class="page-head__description">
{description}
</Body>
)
}
</Flex>
14 changes: 14 additions & 0 deletions astro/src/components/patterns/PageHead/PageHead.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@layer patterns {
.page-head {
margin: var(--space-10) 0;

@media (--md-up) {
margin: var(--space-16) 0 var(--space-12);
}
}

.page-head__description {
max-width: 68rem;
text-align: center;
}
}
7 changes: 7 additions & 0 deletions astro/src/components/patterns/PageHead/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* PageHead Component Index
*
* Export PageHead component
*/

export { default } from './PageHead.astro';
60 changes: 60 additions & 0 deletions astro/src/components/patterns/Pagination/Pagination.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
/**
* Pagination Pattern Component
*
* Renders previous/next navigation with a current page indicator.
*
* @example
* <Pagination
* prevUrl="/blog"
* nextUrl="/blog/3"
* currentPage={2}
* lastPage={5}
* />
*/
import './Pagination.css';
import { Icon } from 'astro-icon/components';
import { Flex, Button, Body } from '@components/primitives';
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
prevUrl?: string;
nextUrl?: string;
currentPage: number;
lastPage: number;
}

const { prevUrl, nextUrl, currentPage, lastPage, ...rest } = Astro.props;
---

<Flex justify="between" align="center" gap="4" class="pagination" {...rest}>
{
prevUrl ? (
<Button as="a" href={prevUrl} variant="secondary" size="md" data-pagination-prev>
<Icon name="fluent:arrow-left-20-filled" />
Previous
</Button>
) : (
<Button variant="secondary" size="md" data-pagination-prev>
<Icon name="fluent:arrow-left-20-filled" />
Previous
</Button>
)
}
<Body vMargin={false} data-pagination-label>
Page {currentPage} of {lastPage}
</Body>
{
nextUrl ? (
<Button as="a" href={nextUrl} variant="secondary" size="md" data-pagination-next>
Next
<Icon name="fluent:arrow-right-20-filled" />
</Button>
) : (
<Button variant="secondary" size="md" data-pagination-next>
Next
<Icon name="fluent:arrow-right-20-filled" />
</Button>
)
}
</Flex>
11 changes: 11 additions & 0 deletions astro/src/components/patterns/Pagination/Pagination.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@layer patterns {
.pagination {
margin-top: var(--space-8);
}

.pagination a[aria-disabled='true'] {
opacity: 0.6;
pointer-events: none;
cursor: not-allowed;
}
}
89 changes: 89 additions & 0 deletions astro/src/components/patterns/PostCard/PostCard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
/**
* PostCard Pattern Component
*
* An article card with a label, title, and author avatar(s).
*
* @example — Single author
* <PostCard
* href="/blog/secure-express-app"
* labels={["Security"]}
* title="How to secure your Express app"
* coverSrc="/images/cover.jpg"
* authors={[{ src: "/images/author.jpg", name: "John Doe" }]}
* avatarCaption="Jun 05, 2025"
* />
*
* @example — Multiple authors
* <PostCard
* href="/blog/express-5"
* labels={["Release"]}
* title="Express 5.0 is here"
* coverSrc="/images/cover.jpg"
* authors={[
* { src: "/images/author1.jpg", name: "Jane Smith" },
* { src: "/images/author2.jpg", name: "John Doe" },
* ]}
* avatarCaption="Mar 04, 2026"
* />
*/
import './PostCard.css';
import type { HTMLAttributes } from 'astro/types';
import { Image } from 'astro:assets';
import { H3, Avatar, Tag } from '@components/primitives';

interface Author {
src: string;
alt?: string;
name: string;
}

interface Props extends HTMLAttributes<'article'> {
href: string;
labels: string[];
title: string;
coverSrc?: string;
coverAlt?: string;
authors: Author[];
avatarCaption?: string;
}

const {
href,
labels,
title,
coverSrc,
coverAlt = '',
authors,
avatarCaption,
class: className,
...rest
} = Astro.props;
---

<article class:list={['post-card', className]} {...rest}>
<a href={href} class="post-card__link">
<div>
<!-- TODO: this will replaced by auto-generated OG images -->
{
coverSrc && (
<Image
class="post-card__cover"
src={coverSrc}
alt={coverAlt || ''}
width={800}
height={160}
loading="eager"
/>
)
}
<div class="post-card__labels">
{labels.map((label) => <Tag>{label}</Tag>)}
</div>
<H3 as="h2" vMargin={false} class="post-card__title">
{title}
</H3>
</div>
<Avatar authors={authors} caption={avatarCaption} size="md" />
</a>
</article>
57 changes: 57 additions & 0 deletions astro/src/components/patterns/PostCard/PostCard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@layer patterns {
.post-card {
display: flex;
flex-direction: column;
border: var(--border-width-1) solid var(--color-border-secondary);
border-radius: var(--radius-base);
gap: var(--space-4);
overflow: hidden;
padding: var(--space-4);
min-height: var(--size-88);
height: 100%;
cursor: pointer;
transition: ease-in-out;
transition-property: outline;

&:hover {
outline: var(--color-focus-ring) solid var(--border-width-1);
}

@media (--md-up) {
min-height: var(--size-100);
}
}

.post-card__labels {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-bottom: var(--space-4);
}

.post-card__link {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: inherit;
color: inherit;
text-decoration: none;
height: 100%;

&:focus {
outline: none;
}
}

.post-card:focus-within {
outline: var(--color-focus-ring) solid var(--border-width-1);
}

.post-card__cover {
margin-bottom: var(--space-3);
display: block;
height: var(--size-40);
width: 100%;
object-fit: cover;
}
}
30 changes: 26 additions & 4 deletions astro/src/components/patterns/ThemeSwitcher/ThemeSwitcher.astro
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,33 @@ const t = useTranslations(lang);
</button>

<script>
// record frame requests
let rafHandle = 0;

const handleToggleClick = () => {
const element = document.documentElement;
const currentTheme = element.getAttribute('data-theme');
localStorage.setItem('theme', currentTheme === 'dark' ? 'light' : 'dark');
document.documentElement.setAttribute('data-theme', currentTheme === 'dark' ? 'light' : 'dark');
const root = document.documentElement;
const nextTheme = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';

// Cancel any pending frame requests to prevent race conditions during rapid clicks
if (rafHandle) {
cancelAnimationFrame(rafHandle);
}

// Disable transitions
root.classList.add('disable-transitions');

// Apply theme
root.setAttribute('data-theme', nextTheme);
localStorage.setItem('theme', nextTheme);

// Wait for the browser to paint the new theme without transitions, then re-enable
// them. Double requestAnimationFrame ensures this happens after a full frame.
rafHandle = requestAnimationFrame(() => {
rafHandle = requestAnimationFrame(() => {
root.classList.remove('disable-transitions');
rafHandle = 0;
});
});
};

document.querySelectorAll('[data-theme-toggle]').forEach((button) => {
Expand Down
3 changes: 3 additions & 0 deletions astro/src/components/patterns/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ export { default as SearchTrigger } from './SearchTrigger/SearchTrigger.astro';
export { default as LanguageSelect } from './LanguageSelect/LanguageSelect.astro';
export { default as Footer } from './Footer/Footer.astro';
export { default as Features } from './Features/Features.astro';
export { default as PostCard } from './PostCard/PostCard.astro';
export { default as Pagination } from './Pagination/Pagination.astro';
export { default as PageHead } from './PageHead/PageHead.astro';
77 changes: 77 additions & 0 deletions astro/src/components/primitives/Avatar/Avatar.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
/**
* Avatar Primitive Component
*
* A rounded avatar image (or stacked images for multiple authors)
* with a name and optional caption displayed to the right.
*
* @example — Single author
* <Avatar
* authors={[{ src: "/images/author.jpg", name: "John Doe" }]}
* caption="Jun 05, 2025"
* />
*
* @example — Multiple authors
* <Avatar
* authors={[
* { src: "/images/a.jpg", name: "Alex Pit" },
* { src: "/images/b.jpg", name: "Tom Cruise" },
* ]}
* caption="Mar 04, 2026"
* />
*
* @example — Small size
* <Avatar
* authors={[{ src: "/images/author.jpg", name: "Jane Smith" }]}
* caption="Mar 04, 2026"
* size="sm"
* />
*/
import './Avatar.css';
import type { HTMLAttributes } from 'astro/types';
import { Image } from 'astro:assets';
import { BodyMd, BodySm, Flex } from '@components/primitives';

interface Author {
src: string;
alt?: string;
name: string;
}

interface Props extends HTMLAttributes<'div'> {
authors: Author[];
caption?: string;
size?: 'sm' | 'md';
}

const { authors, caption, size = 'md', class: className, ...rest } = Astro.props;

const displayName = authors?.map((a) => a.name).join(', ') ?? '';
const imgSize = size === 'sm' ? 24 : 32;
---

<Flex align="center" gap="2" class:list={[`avatar--${size}`, className]} {...rest}>
<div class="avatar__images">
{
authors?.map((a) => (
<Image
class="avatar__image"
src={a.src}
alt={a.alt ?? a.name}
width={imgSize}
height={imgSize}
/>
))
}
</div>
<div class="avatar__content">
<BodyMd as="span" weight="medium" vMargin={false}>{displayName}</BodyMd>
{
caption && (
<BodySm as="span" color="secondary" vMargin={false}>
{caption}
</BodySm>
)
}
</div>
</Flex>
Loading