Skip to content

Commit a3eb903

Browse files
authored
Update post to show breadcrumbs and date (#9)
* Show post title in page title * Add date and breadcrumbs to post * Increase post line height * Show date for post on home * Format date in DD MMM YYYY format
1 parent 1b297e9 commit a3eb903

8 files changed

Lines changed: 111 additions & 19 deletions

File tree

cms/content/post/website-launch.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
title: Website launch
3+
date: 2026-01-19
34
---
45

56
In the past week I've launched this website. It's structured as a monorepo with cms and web projects. Not only does this separate concerns, it will allow me to add or replace a project if needed.

web/src/App.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
}
1919

2020
h1 {
21-
font-size: clamp(2rem, 4vw, 3rem);
21+
font-size: clamp(1.5rem, 4vw, 2rem);
2222
margin: 0;
2323
letter-spacing: -0.02em;
2424
}
@@ -99,6 +99,7 @@ h1 {
9999
display: flex;
100100
justify-content: space-between;
101101
align-items: baseline;
102+
gap: 16px;
102103
}
103104

104105
.section-header h2 {

web/src/Home.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { Link } from 'react-router-dom'
12
import { ArrowTopRightIcon } from '@radix-ui/react-icons'
23
import * as Separator from '@radix-ui/react-separator'
3-
import { Link } from 'react-router-dom'
4+
import { formatPostDate } from './utils'
45
import { links, posts, projects } from './data'
56

67
function Hero() {
@@ -36,18 +37,23 @@ function Posts() {
3637
*/}
3738
</div>
3839
<div className="list">
39-
{posts.map((post) => (
40-
<article key={post.slug} className="list-item">
41-
<div>
42-
<h3>
43-
<Link to={`/posts/${post.slug}`}>
44-
{post.title} <ArrowTopRightIcon aria-hidden="true" />
45-
</Link>
46-
</h3>
47-
<p>{post.description}</p>
48-
</div>
49-
</article>
50-
))}
40+
{posts.map((post) => {
41+
const postDate = formatPostDate(post.date)
42+
43+
return (
44+
<article key={post.slug} className="list-item">
45+
<div>
46+
<h3>
47+
<Link to={`/posts/${post.slug}`}>
48+
{post.title} <ArrowTopRightIcon aria-hidden="true" />
49+
</Link>
50+
</h3>
51+
<p>{post.description}</p>
52+
</div>
53+
{postDate ? <span className="meta">{postDate}</span> : null}
54+
</article>
55+
)
56+
})}
5157
</div>
5258
</section>
5359
)

web/src/Post.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
.breadcrumbs {
2+
display: flex;
3+
align-items: center;
4+
gap: 10px;
5+
font-size: 0.85rem;
6+
letter-spacing: 0.04em;
7+
color: #7f8693;
8+
}
9+
10+
.breadcrumbs-separator {
11+
color: #343a44;
12+
}
13+
14+
.post-header {
15+
display: flex;
16+
flex-direction: column;
17+
gap: 20px;
18+
margin-bottom: 24px;
19+
}
20+
21+
.post-header .breadcrumbs {
22+
margin-bottom: 24px;
23+
}
24+
25+
.post-title {
26+
display: flex;
27+
flex-direction: column;
28+
gap: 8px;
29+
}
30+
31+
.post-body {
32+
font-size: 1rem;
33+
line-height: 2;
34+
color: #c7cbd3;
35+
}
36+
37+
.post-body p {
38+
margin: 0 0 20px;
39+
}
40+
41+
.post-body p:last-child {
42+
margin-bottom: 0;
43+
}

web/src/Post.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
import { useEffect } from 'react'
12
import { Link, useParams } from 'react-router-dom'
23
import { posts } from './data'
4+
import { formatPostDate } from './utils'
5+
import './Post.css'
36

47
function Post() {
58
const { slug } = useParams()
69
const post = posts.find((entry) => entry.slug === slug)
10+
const postDate = formatPostDate(post?.date)
11+
12+
useEffect(() => {
13+
if (!post) return
14+
document.title = `${post.title} - Shaun Molloy`
15+
}, [post])
716

817
if (!post) {
918
return (
@@ -19,11 +28,20 @@ function Post() {
1928

2029
return (
2130
<section className="section section--thin">
22-
<div className="section-header">
23-
<h1>{post.title}</h1>
24-
<Link className="muted-link" to="/">
25-
Back to home
26-
</Link>
31+
<div className="post-header">
32+
<nav className="breadcrumbs" aria-label="Breadcrumb">
33+
<Link className="muted-link" to="/">
34+
Home
35+
</Link>
36+
<span className="breadcrumbs-separator" aria-hidden="true">
37+
/
38+
</span>
39+
<span className="breadcrumbs-current">Posts</span>
40+
</nav>
41+
<div className="post-title">
42+
<h1>{post.title}</h1>
43+
{postDate ? <p className="meta">{postDate}</p> : null}
44+
</div>
2745
</div>
2846
<div className="post-body">
2947
{post.content.split(/\n\s*\n/).map((paragraph, index) => (

web/src/data.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ const parseFrontmatter = (content: string): PostFrontmatter => {
2424
}
2525

2626
const titleMatch = frontmatterMatch[1].match(/title:\s*['"]?(.+?)['"]?\s*$/m)
27+
const dateMatch = frontmatterMatch[1].match(/date:\s*['"]?(.+?)['"]?\s*$/m)
2728
return {
2829
title: titleMatch?.[1]?.trim() ?? 'Untitled',
30+
date: dateMatch?.[1]?.trim(),
2931
}
3032
}
3133

@@ -39,6 +41,7 @@ export const posts: PostEntry[] = Object.entries(postModules).map(([path, conten
3941
return {
4042
slug,
4143
title: frontmatter.title,
44+
date: frontmatter.date,
4245
description: '',
4346
content: stripFrontmatter(content).trim(),
4447
}

web/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
export type PostFrontmatter = {
22
title: string
3+
date?: string
34
}
45

56
export type PostEntry = {
67
slug: string
78
title: string
9+
date?: string
810
description: string
911
content: string
1012
}

web/src/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const formatPostDate = (value?: string) => {
2+
if (!value) return null
3+
4+
const date = new Date(value)
5+
if (Number.isNaN(date.getTime())) return null
6+
7+
const parts = new Intl.DateTimeFormat('en-GB', {
8+
day: '2-digit',
9+
month: 'short',
10+
year: 'numeric',
11+
}).formatToParts(date)
12+
13+
const day = parts.find((part) => part.type === 'day')?.value
14+
const month = parts.find((part) => part.type === 'month')?.value
15+
const year = parts.find((part) => part.type === 'year')?.value
16+
17+
return day && month && year ? `${day} ${month} ${year}` : null
18+
}

0 commit comments

Comments
 (0)