Skip to content

Commit 006257d

Browse files
committed
feat: implement contributor and author components
1 parent bae2a77 commit 006257d

17 files changed

Lines changed: 449 additions & 10 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
PUBLIC_WEBSITE_URL=http://localhost:4323
55
PUBLIC_DOCS_URL=http://localhost:4321
66
PUBLIC_BLOG_URL=http://localhost:4322
7+
8+
# Optional: GitHub token for higher API rate limits (contributors fetch)
9+
# GITHUB_TOKEN=ghp_xxxxxxxxxxxx

.github/workflows/deploy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ jobs:
6161
PUBLIC_DOCS_URL: ${{ vars.PUBLIC_DOCS_URL }}
6262
PUBLIC_BLOG_URL: ${{ vars.PUBLIC_BLOG_URL }}
6363
PUBLIC_WEBSITE_URL: ${{ vars.PUBLIC_WEBSITE_URL }}
64+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6465
- uses: actions/upload-artifact@v4
6566
with:
6667
name: docs-dist
@@ -83,6 +84,7 @@ jobs:
8384
PUBLIC_BLOG_URL: ${{ vars.PUBLIC_BLOG_URL }}
8485
PUBLIC_DOCS_URL: ${{ vars.PUBLIC_DOCS_URL }}
8586
PUBLIC_WEBSITE_URL: ${{ vars.PUBLIC_WEBSITE_URL }}
87+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8688
- uses: actions/upload-artifact@v4
8789
with:
8890
name: blog-dist

apps/blog/astro.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { remarkAutoImport } from '@explainer/mdx/remark-auto-import'
99
import { remarkDirectiveHandler } from '@explainer/mdx/remark-directive-handler'
1010
import { remarkCodeBlocks } from '@explainer/mdx/remark-code-blocks'
1111
import { thumbnailIntegration } from '@explainer/thumbnail/integration'
12+
import { remarkDocsLinks } from './src/plugins/remark-docs-links'
1213

1314
function loadRootEnv() {
1415
try {
@@ -31,7 +32,13 @@ export default defineConfig({
3132
integrations: [
3233
react(),
3334
mdx({
34-
remarkPlugins: [remarkAutoImport, remarkCodeBlocks, remarkDirective, remarkDirectiveHandler],
35+
remarkPlugins: [
36+
remarkAutoImport,
37+
remarkCodeBlocks,
38+
remarkDirective,
39+
[remarkDocsLinks, { docsUrl: process.env.PUBLIC_DOCS_URL || env.PUBLIC_DOCS_URL }],
40+
remarkDirectiveHandler,
41+
],
3542
}),
3643
thumbnailIntegration({
3744
appName: 'Blog',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { cn } from '@explainer/ui'
2+
3+
export interface AuthorCardProps {
4+
author: {
5+
name: string
6+
title: string
7+
avatar: string
8+
href?: string
9+
}
10+
label?: string
11+
className?: string
12+
}
13+
14+
export function AuthorCard({ author, label = 'Written by', className }: AuthorCardProps) {
15+
const content = (
16+
<div className="flex items-center gap-3">
17+
<img
18+
src={author.avatar}
19+
alt={author.name}
20+
className="h-10 w-10 rounded-full ring-1 ring-border shrink-0"
21+
loading="lazy"
22+
/>
23+
<div className="min-w-0">
24+
<p className="text-sm font-medium truncate">{author.name}</p>
25+
<p className="text-xs text-muted-foreground truncate">{author.title}</p>
26+
</div>
27+
</div>
28+
)
29+
30+
return (
31+
<div className={cn('border-t border-dashed border-border pt-4 mt-4', className)}>
32+
<p className="text-sm font-medium mb-3">{label}</p>
33+
{author.href ? (
34+
<a
35+
href={author.href}
36+
target="_blank"
37+
rel="noopener noreferrer"
38+
className="block rounded-md p-2 -mx-2 transition-colors hover:bg-accent/50"
39+
>
40+
{content}
41+
</a>
42+
) : (
43+
<div className="p-2 -mx-2">{content}</div>
44+
)}
45+
</div>
46+
)
47+
}

apps/blog/src/components/TableOfContents.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
import * as React from 'react'
22
import { cn, SponsorCards, defaultSponsors } from '@explainer/ui'
33
import { useTranslations } from '../i18n/utils'
4+
import { AuthorCard } from './AuthorCard'
45

56
export interface TocHeading {
67
depth: number
78
slug: string
89
text: string
910
}
1011

12+
interface Author {
13+
name: string
14+
title: string
15+
avatar: string
16+
href?: string
17+
}
18+
1119
interface TableOfContentsProps {
1220
headings: TocHeading[]
1321
locale?: string
22+
author?: Author
1423
}
1524

16-
export function TableOfContents({ headings, locale = 'en' }: TableOfContentsProps) {
25+
export function TableOfContents({ headings, locale = 'en', author }: TableOfContentsProps) {
1726
const t = useTranslations(locale)
1827
const filtered = headings.filter((h) => h.depth >= 2 && h.depth <= 3)
1928
const [activeId, setActiveId] = React.useState<string>(filtered[0]?.slug ?? '')
@@ -64,6 +73,7 @@ export function TableOfContents({ headings, locale = 'en' }: TableOfContentsProp
6473
</li>
6574
))}
6675
</ul>
76+
{author && <AuthorCard author={author} label={t('author.label')} />}
6777
<SponsorCards sponsors={defaultSponsors} title={t('sponsors.title')} />
6878
</nav>
6979
)

apps/blog/src/i18n/ui.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const ui = {
3636
// Sponsors
3737
'sponsors.title': 'Sponsors',
3838

39+
// Author
40+
'author.label': 'Written by',
41+
3942
// Share buttons
4043
'share.linkedin': 'Share on LinkedIn',
4144
'share.twitter': 'Share on Twitter',
@@ -77,6 +80,9 @@ export const ui = {
7780
// Sponsors
7881
'sponsors.title': 'Sponsors',
7982

83+
// Author
84+
'author.label': 'Écrit par',
85+
8086
// Share buttons
8187
'share.linkedin': 'Partager sur LinkedIn',
8288
'share.twitter': 'Partager sur Twitter',

apps/blog/src/layouts/post.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ interface Props {
2121
readingTime: number
2222
headings: Heading[]
2323
url: string
24+
author?: { name: string; title: string; avatar: string; href?: string }
2425
locale?: string
2526
locales?: string[]
2627
localeSwitchUrls?: Record<string, string>
2728
}
2829
29-
const { title, description, date, tags, cover, thumbnail, readingTime, headings, url, locale = 'en', locales, localeSwitchUrls } = Astro.props
30+
const { title, description, date, tags, cover, thumbnail, readingTime, headings, url, author, locale = 'en', locales, localeSwitchUrls } = Astro.props
3031
const t = useTranslations(locale)
3132
---
3233

@@ -82,7 +83,7 @@ const t = useTranslations(locale)
8283

8384
<!-- Right column: Table of Contents (sticky) -->
8485
<aside class="hidden xl:block w-56 shrink-0">
85-
<TableOfContents headings={headings} locale={locale} client:load />
86+
<TableOfContents headings={headings} locale={locale} author={author} client:load />
8687
</aside>
8788
</div>
8889
</Base>

apps/blog/src/pages/[locale]/[slug].astro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { getCollection, render } from 'astro:content'
33
import PostLayout from '../../layouts/post.astro'
44
import { getPublishedPosts, getPostSlug, getPostLocale, getPostHref, getLocales, getReadingTime, buildLocaleSwitchUrls } from '../../lib/posts'
5+
import { getAuthor } from '../../lib/authors'
56
import Pre from '@explainer/mdx/overrides/pre.astro'
67
import Blockquote from '@explainer/mdx/overrides/blockquote.astro'
78
import MathBlock from '@explainer/mdx/components/math.astro'
@@ -26,6 +27,7 @@ const { Content, headings } = await render(post)
2627
const readingTime = getReadingTime(post.body ?? '')
2728
const articleUrl = new URL(getPostHref(post), Astro.site ?? Astro.url.origin).href
2829
const localeSwitchUrls = buildLocaleSwitchUrls(allPosts, post, locales)
30+
const author = post.data.author ? getAuthor(post.data.author) : undefined
2931
---
3032

3133
<PostLayout
@@ -38,6 +40,7 @@ const localeSwitchUrls = buildLocaleSwitchUrls(allPosts, post, locales)
3840
readingTime={readingTime}
3941
headings={headings}
4042
url={articleUrl}
43+
author={author}
4144
locale={locale}
4245
locales={locales}
4346
localeSwitchUrls={localeSwitchUrls}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Root } from 'mdast'
2+
3+
const docsPathPattern = /^\/(en|fr)\/explainer\//
4+
5+
function visitNodes(node: any, visitor: (n: any) => void) {
6+
visitor(node)
7+
if (node.children) {
8+
for (const child of node.children) {
9+
visitNodes(child, visitor)
10+
}
11+
}
12+
}
13+
14+
/**
15+
* Remark plugin that rewrites internal docs links to point to the docs app URL.
16+
* Links matching `/(en|fr)/explainer/...` are prefixed with the docs base URL.
17+
*/
18+
export function remarkDocsLinks(options?: { docsUrl?: string }) {
19+
const docsUrl = (options?.docsUrl ?? '').replace(/\/$/, '')
20+
21+
return (tree: Root) => {
22+
if (!docsUrl) return
23+
24+
visitNodes(tree, (node: any) => {
25+
// Rewrite markdown links: [text](/en/explainer/...)
26+
if (node.type === 'link' && typeof node.url === 'string' && docsPathPattern.test(node.url)) {
27+
node.url = docsUrl + node.url
28+
}
29+
30+
// Rewrite directive href attributes: :::card{href="/en/explainer/..."}
31+
if (
32+
(node.type === 'containerDirective' || node.type === 'leafDirective') &&
33+
node.attributes?.href &&
34+
docsPathPattern.test(node.attributes.href)
35+
) {
36+
node.attributes.href = docsUrl + node.attributes.href
37+
}
38+
})
39+
}
40+
}

apps/docs/src/components/toc.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cn, defaultSponsors, SponsorCards } from '@explainer/ui'
1+
import { cn, defaultSponsors, SponsorCards, ContributorCards, type Contributor } from '@explainer/ui'
22
import * as React from 'react'
33

44
export interface TocHeading {
@@ -9,9 +9,10 @@ export interface TocHeading {
99

1010
interface TocProps {
1111
headings: TocHeading[]
12+
contributors?: Contributor[]
1213
}
1314

14-
export function TableOfContents({ headings }: TocProps) {
15+
export function TableOfContents({ headings, contributors = [] }: TocProps) {
1516
const filtered = headings.filter((h) => h.depth >= 2 && h.depth <= 3)
1617
const [activeId, setActiveId] = React.useState<string>('')
1718

@@ -73,6 +74,7 @@ export function TableOfContents({ headings }: TocProps) {
7374
))}
7475
</ul>
7576
<SponsorCards sponsors={defaultSponsors} />
77+
<ContributorCards contributors={contributors} />
7678
</nav>
7779
)
7880
}

0 commit comments

Comments
 (0)