Skip to content

Commit c1199ec

Browse files
committed
Wire readingTIme prop in to script, add Python dependabot script, update styling on avatar with no image
1 parent 55e54d3 commit c1199ec

7 files changed

Lines changed: 119 additions & 12 deletions

File tree

.github/dependabot.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,16 @@ updates:
2222
github-actions:
2323
patterns:
2424
- "*"
25+
26+
- package-ecosystem: "pip"
27+
directory: "/"
28+
schedule:
29+
interval: "weekly"
30+
open-pull-requests-limit: 1
31+
labels:
32+
- "type: dependencies 🔗"
33+
- "automerge 🤞"
34+
groups:
35+
python-dependencies:
36+
patterns:
37+
- "*"

src/components/Avatar/index.astro

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ if (!avatarImage && !name) {
4141
4242
const avatarAlt = name
4343
44-
const avatarSizeClass = lead ? 'w-10 h-10' : 'w-16 h-16'
44+
const avatarSizeClass = lead ? 'w-10 h-10 text-xl' : 'w-16 h-16 text-2xl'
4545
---
4646

4747
<div class:list={['avatar-container', avatarSizeClass, 'rounded-full overflow-hidden', className]}>
@@ -59,9 +59,11 @@ const avatarSizeClass = lead ? 'w-10 h-10' : 'w-16 h-16'
5959
<div
6060
class:list={[
6161
'avatar-placeholder',
62-
'bg-spotlight',
62+
'bg-secondary',
6363
avatarSizeClass,
64-
'rounded-full flex items-center justify-center text-content-offset',
64+
'rounded-full',
65+
'flex items-center justify-center',
66+
'font-bold tracking-widest text-content-inverse',
6567
]}
6668
role="img"
6769
aria-label={name}

src/components/Test/Demo.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
*/
66
---
77

8-
And a [link]('/about') to included text.
8+
And a [link](/about) to included text.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { getReadingTimeLabel } from '@lib/markdown/utils/readingTime'
3+
4+
describe('getReadingTimeLabel', () => {
5+
it('returns undefined for empty content', () => {
6+
expect(getReadingTimeLabel('')).toBeUndefined()
7+
})
8+
9+
it('returns at least 1 minute for short content', () => {
10+
expect(getReadingTimeLabel('hello world')).toBe('1 min read')
11+
})
12+
13+
it('rounds up minutes based on word count', () => {
14+
const twoHundredAndOneWords = Array.from({ length: 201 }, () => 'word').join(' ')
15+
expect(getReadingTimeLabel(twoHundredAndOneWords, { wordsPerMinute: 200 })).toBe('2 min read')
16+
})
17+
18+
it('strips fenced code blocks and mdx imports', () => {
19+
const content = `---
20+
title: Test
21+
---
22+
import X from './X.mdx'
23+
24+
Hello world
25+
26+
\`\`\`ts
27+
const secret = 123
28+
\`\`\`
29+
`
30+
31+
expect(getReadingTimeLabel(content, { wordsPerMinute: 200 })).toBe('1 min read')
32+
})
33+
})
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Estimate reading time for Markdown/MDX content.
3+
*
4+
* This is intentionally lightweight (no external deps) and runs at build-time.
5+
* It strips common Markdown/MDX constructs (frontmatter, code blocks, tags, imports)
6+
* before counting words.
7+
*/
8+
9+
const defaultWordsPerMinute = 200
10+
11+
export function getReadingTimeLabel(
12+
content: string,
13+
options?: {
14+
wordsPerMinute?: number
15+
}
16+
): string | undefined {
17+
const wordsPerMinute = options?.wordsPerMinute ?? defaultWordsPerMinute
18+
if (!Number.isFinite(wordsPerMinute) || wordsPerMinute <= 0) return undefined
19+
20+
const wordCount = countWords(stripMarkdownForReadingTime(content))
21+
if (wordCount <= 0) return undefined
22+
23+
const minutes = Math.max(1, Math.ceil(wordCount / wordsPerMinute))
24+
return `${minutes} min read`
25+
}
26+
27+
function stripMarkdownForReadingTime(content: string): string {
28+
// Remove YAML frontmatter if present (defensive; Astro collection body usually excludes it).
29+
const withoutFrontmatter = content.replace(/^---\s*[\s\S]*?\s*---\s*/m, ' ')
30+
31+
// Remove fenced code blocks.
32+
const withoutFences = withoutFrontmatter.replace(/```[\s\S]*?```/g, ' ')
33+
34+
// Remove inline code.
35+
const withoutInlineCode = withoutFences.replace(/`[^`]*`/g, ' ')
36+
37+
// Remove MDX/ESM imports/exports.
38+
const withoutImports = withoutInlineCode
39+
.replace(/^\s*import\s+[^;\n]+;?\s*$/gm, ' ')
40+
.replace(/^\s*export\s+[^;\n]+;?\s*$/gm, ' ')
41+
42+
// Remove JSX/HTML tags.
43+
const withoutTags = withoutImports.replace(/<[^>]+>/g, ' ')
44+
45+
// Collapse links/images to their visible text.
46+
const withoutImages = withoutTags.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
47+
const withoutLinks = withoutImages.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
48+
49+
return withoutLinks
50+
}
51+
52+
function countWords(text: string): number {
53+
const matches = text.match(/[\p{L}\p{N}]+(?:['][\p{L}\p{N}]+)*/gu)
54+
return matches ? matches.length : 0
55+
}

src/pages/articles/[...slug].astro

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { type CollectionEntry, getCollection, getEntry } from 'astro:content'
33
import { Picture } from 'astro:assets'
44
import { isDev } from '@lib/config/environmentServer'
5+
import { getReadingTimeLabel } from '@lib/markdown/utils/readingTime'
56
import MarkdownLayout from '@layouts/MarkdownLayout.astro'
67
import Shares from '@components/Social/Shares/index.astro'
78
import Carousel from '@components/Carousel/index.astro'
@@ -25,6 +26,9 @@ export async function getStaticPaths() {
2526
const { article } = Astro.props
2627
const author = await getEntry(article.data.author)
2728
29+
const articleBody = article.body ?? ''
30+
const readingTime = article.data.readingTime ?? getReadingTimeLabel(articleBody)
31+
2832
const path = `/articles/${article.id}`
2933
const section = `Articles`
3034
---
@@ -42,7 +46,7 @@ const section = `Articles`
4246
section={section}
4347
showToc={article.data.showToc ?? true}
4448
tags={article.data.tags?.map(t => (typeof t === 'string' ? t : t.id)) ?? []}
45-
{...article.data.readingTime && { readingTime: article.data.readingTime }}
49+
{...readingTime && { readingTime }}
4650
>
4751
{/** SLOT: hero-image */}
4852
{

src/styles/general.css

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
* plugin, shows when hovering over the heading
44
*/
55
.anchor-link {
6-
margin-left: 0.2rem;
76
display: inline-block;
7+
margin-left: 0.2rem;
88
opacity: 0;
9-
transition: opacity 0.2s ease-in-out;
109
transform: translateY(-4px);
10+
transition: opacity 0.2s ease-in-out;
1111
}
1212

1313
/* Ensure the injected heading anchor never inherits the global dotted underline. */
@@ -17,11 +17,6 @@
1717
text-decoration: none;
1818
}
1919

20-
/* Also show when the *anchor* itself is focused (keyboard navigation/click focus). */
21-
.heading-anchor:focus-visible .anchor-link {
22-
opacity: 1;
23-
}
24-
2520
h1:hover .anchor-link,
2621
h2:hover .anchor-link,
2722
h3:hover .anchor-link,
@@ -30,3 +25,8 @@ h5:hover .anchor-link,
3025
h6:hover .anchor-link {
3126
opacity: 1;
3227
}
28+
29+
/* Also show when the *anchor* itself is focused (keyboard navigation/click focus). */
30+
.heading-anchor:focus-visible .anchor-link {
31+
opacity: 1;
32+
}

0 commit comments

Comments
 (0)