Skip to content

Commit 58e615d

Browse files
authored
Merge pull request #1643 from recodehive/copilot/bring-tag-change-to-blog-content
Add compact author meta bar and colored tag pills to blog post content page
2 parents 1afde77 + fe3b3c0 commit 58e615d

3 files changed

Lines changed: 338 additions & 1 deletion

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import React from "react";
2+
import Link from "@docusaurus/Link";
3+
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
4+
import { useBlogPost } from "@docusaurus/plugin-content-blog/client";
5+
import BlogPostItemHeaderOriginal from "@theme-original/BlogPostItem/Header";
6+
import BlogPostItemHeaderTitle from "@theme/BlogPostItem/Header/Title";
7+
import type BlogPostItemHeaderType from "@theme/BlogPostItem/Header";
8+
import type { WrapperProps } from "@docusaurus/types";
9+
import { getAuthorProfile } from "../../../utils/authors";
10+
11+
import styles from "./styles.module.css";
12+
13+
type Props = WrapperProps<typeof BlogPostItemHeaderType>;
14+
15+
const TAG_COLORS = [
16+
{ dot: "#f59e0b", border: "#fde68a", bg: "#fffbeb", text: "#92400e" },
17+
{ dot: "#6366f1", border: "#c7d2fe", bg: "#eef2ff", text: "#3730a3" },
18+
{ dot: "#ec4899", border: "#fbcfe8", bg: "#fdf2f8", text: "#9d174d" },
19+
{ dot: "#10b981", border: "#a7f3d0", bg: "#ecfdf5", text: "#065f46" },
20+
{ dot: "#f97316", border: "#fed7aa", bg: "#fff7ed", text: "#9a3412" },
21+
{ dot: "#8b5cf6", border: "#ddd6fe", bg: "#f5f3ff", text: "#5b21b6" },
22+
{ dot: "#14b8a6", border: "#99f6e4", bg: "#f0fdfa", text: "#134e4a" },
23+
{ dot: "#ef4444", border: "#fecaca", bg: "#fef2f2", text: "#991b1b" },
24+
];
25+
26+
function tagColor(label: string) {
27+
let hash = 0;
28+
for (let i = 0; i < label.length; i++) {
29+
hash = (hash * 31 + label.charCodeAt(i)) & 0xffff;
30+
}
31+
return TAG_COLORS[hash % TAG_COLORS.length];
32+
}
33+
34+
function getGitHubHandle(author: { key?: string; url?: string }): string | undefined {
35+
if (author.url && /github\.com\//i.test(author.url)) {
36+
const matched = author.url.match(/github\.com\/([^/?#]+)/i);
37+
if (matched?.[1]) {
38+
return `@${matched[1]}`;
39+
}
40+
}
41+
if (author.key) {
42+
return `@${author.key}`;
43+
}
44+
return undefined;
45+
}
46+
47+
function getGitHubUrl(author: { key?: string; url?: string; name?: string }): string | undefined {
48+
if (author.key) {
49+
return getAuthorProfile(author.key).githubUrl;
50+
}
51+
if (author.url && /github\.com\//i.test(author.url)) {
52+
return author.url.startsWith("http") ? author.url : `https://${author.url}`;
53+
}
54+
return undefined;
55+
}
56+
57+
export default function BlogPostItemHeaderWrapper(props: Props): JSX.Element {
58+
const { siteConfig } = useDocusaurusContext();
59+
const { metadata, isBlogPostPage } = useBlogPost();
60+
61+
if (!isBlogPostPage) {
62+
return <BlogPostItemHeaderOriginal {...props} />;
63+
}
64+
65+
// Build display data for ALL authors (not just the first one)
66+
const authors = (metadata.authors ?? []).map((author) => {
67+
const profile = author.key ? getAuthorProfile(author.key) : undefined;
68+
return {
69+
avatar: author.imageURL || profile?.imageUrl,
70+
name: author.name || profile?.name,
71+
handle: getGitHubHandle({ key: author.key, url: author.url }),
72+
url: getGitHubUrl({ key: author.key, url: author.url, name: author.name }),
73+
};
74+
});
75+
76+
const roundedReadTime = Math.max(1, Math.ceil(metadata.readingTime || 0));
77+
const readTimeText = `${roundedReadTime} min read`;
78+
79+
const blogDate = metadata.date
80+
? new Intl.DateTimeFormat(siteConfig.i18n?.defaultLocale || "en-US", {
81+
year: "numeric",
82+
month: "short",
83+
day: "numeric",
84+
}).format(new Date(metadata.date))
85+
: undefined;
86+
87+
const tags = (metadata.tags ?? [])
88+
.map((t) => (typeof t === "string" ? t : (t as { label?: string }).label ?? ""))
89+
.filter(Boolean);
90+
91+
return (
92+
<>
93+
{/* Render only the title — Info and Authors are replaced by our compact bar */}
94+
<BlogPostItemHeaderTitle />
95+
<div className={styles.metaSection}>
96+
{/* Compact meta row: avatars · @handles · date · reading time */}
97+
<div className={styles.metaRow}>
98+
{authors.length > 0 && (
99+
<div className={styles.authorsPart}>
100+
{authors.map((author, idx) => (
101+
<React.Fragment key={author.handle ?? `${author.name ?? ""}-${idx}`}>
102+
{idx > 0 && (
103+
<span className={styles.authorSep} aria-hidden="true">
104+
&amp;
105+
</span>
106+
)}
107+
<div className={styles.authorPart}>
108+
{author.avatar ? (
109+
<img
110+
className={styles.avatar}
111+
src={author.avatar}
112+
alt={author.name ? `${author.name} avatar` : "Author avatar"}
113+
loading="lazy"
114+
/>
115+
) : (
116+
<div className={styles.avatarFallback} aria-hidden="true">
117+
{author.name?.charAt(0).toUpperCase()}
118+
</div>
119+
)}
120+
{author.handle &&
121+
(author.url ? (
122+
<Link
123+
to={author.url}
124+
className={styles.handle}
125+
target="_blank"
126+
rel="noopener noreferrer"
127+
>
128+
{author.handle}
129+
</Link>
130+
) : (
131+
<span className={styles.handle}>{author.handle}</span>
132+
))}
133+
</div>
134+
</React.Fragment>
135+
))}
136+
</div>
137+
)}
138+
139+
{blogDate && (
140+
<>
141+
<span className={styles.sep} aria-hidden="true">
142+
143+
</span>
144+
<span className={styles.date}>{blogDate}</span>
145+
</>
146+
)}
147+
148+
<span className={styles.sep} aria-hidden="true">
149+
150+
</span>
151+
<span className={styles.readTime}>
152+
<svg
153+
xmlns="http://www.w3.org/2000/svg"
154+
viewBox="0 0 24 24"
155+
width="13"
156+
height="13"
157+
fill="none"
158+
stroke="currentColor"
159+
strokeWidth="2"
160+
strokeLinecap="round"
161+
strokeLinejoin="round"
162+
aria-hidden="true"
163+
>
164+
<circle cx="12" cy="12" r="10" />
165+
<polyline points="12 6 12 12 16 14" />
166+
</svg>
167+
{readTimeText}
168+
</span>
169+
</div>
170+
171+
{/* Colored tag pills */}
172+
{tags.length > 0 && (
173+
<div className={styles.tagsRow} aria-label="Tags">
174+
{tags.map((tag) => {
175+
const c = tagColor(tag);
176+
return (
177+
<span
178+
key={tag}
179+
className={styles.tag}
180+
style={
181+
{
182+
"--tag-dot": c.dot,
183+
"--tag-border": c.border,
184+
"--tag-bg": c.bg,
185+
"--tag-text": c.text,
186+
} as React.CSSProperties
187+
}
188+
>
189+
<span className={styles.tagDot} aria-hidden="true" />
190+
{tag}
191+
</span>
192+
);
193+
})}
194+
</div>
195+
)}
196+
</div>
197+
</>
198+
);
199+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/* Blog post page: compact meta bar + tag pills */
2+
3+
.metaSection {
4+
display: flex;
5+
flex-direction: column;
6+
align-items: center;
7+
gap: 0.65rem;
8+
margin-top: 0.75rem;
9+
margin-bottom: 1.25rem;
10+
padding-bottom: 1.25rem;
11+
border-bottom: 1px solid var(--ifm-color-emphasis-200);
12+
}
13+
14+
/* ── Meta row: avatar · @handle · date · reading time ── */
15+
.metaRow {
16+
display: flex;
17+
align-items: center;
18+
justify-content: center;
19+
flex-wrap: wrap;
20+
gap: 0.45rem;
21+
font-size: 0.9rem;
22+
color: var(--ifm-color-emphasis-700);
23+
}
24+
25+
.authorsPart {
26+
display: flex;
27+
align-items: center;
28+
flex-wrap: wrap;
29+
gap: 0.45rem;
30+
}
31+
32+
.authorSep {
33+
color: var(--ifm-color-emphasis-500);
34+
font-size: 0.8rem;
35+
font-weight: 500;
36+
}
37+
38+
.authorPart {
39+
display: flex;
40+
align-items: center;
41+
gap: 0.45rem;
42+
}
43+
44+
.avatar,
45+
.avatarFallback {
46+
width: 32px;
47+
height: 32px;
48+
border-radius: 50%;
49+
flex-shrink: 0;
50+
}
51+
52+
.avatar {
53+
object-fit: cover;
54+
border: 1px solid var(--ifm-color-emphasis-300);
55+
}
56+
57+
.avatarFallback {
58+
display: grid;
59+
place-items: center;
60+
font-weight: 700;
61+
font-size: 0.8rem;
62+
background: var(--ifm-color-emphasis-200);
63+
color: var(--ifm-color-primary-darkest);
64+
}
65+
66+
.handle {
67+
font-weight: 600;
68+
color: var(--ifm-color-primary);
69+
text-decoration: none;
70+
}
71+
72+
.handle:hover {
73+
text-decoration: underline;
74+
}
75+
76+
.sep {
77+
color: var(--ifm-color-emphasis-400);
78+
font-size: 0.8rem;
79+
line-height: 1;
80+
}
81+
82+
.date {
83+
color: var(--ifm-color-emphasis-700);
84+
}
85+
86+
.readTime {
87+
display: inline-flex;
88+
align-items: center;
89+
gap: 0.35rem;
90+
padding: 3px 10px 3px 8px;
91+
border-radius: 20px;
92+
font-size: 0.8rem;
93+
font-weight: 500;
94+
color: var(--ifm-color-emphasis-700);
95+
background: var(--ifm-color-emphasis-100);
96+
border: 1px solid var(--ifm-color-emphasis-300);
97+
}
98+
99+
/* ── Tag pills ── */
100+
.tagsRow {
101+
display: flex;
102+
flex-wrap: wrap;
103+
justify-content: center;
104+
gap: 0.45rem;
105+
}
106+
107+
.tag {
108+
display: inline-flex;
109+
align-items: center;
110+
gap: 0.35rem;
111+
padding: 3px 10px 3px 8px;
112+
border-radius: 20px;
113+
font-size: 0.8rem;
114+
font-weight: 500;
115+
background: var(--tag-bg);
116+
color: var(--tag-text);
117+
border: 1px solid var(--tag-border);
118+
white-space: nowrap;
119+
}
120+
121+
.tagDot {
122+
width: 6px;
123+
height: 6px;
124+
border-radius: 50%;
125+
background: var(--tag-dot);
126+
flex-shrink: 0;
127+
}
128+
129+
[data-theme="dark"] .tag {
130+
opacity: 0.85;
131+
}
132+
133+
@media (max-width: 480px) {
134+
.metaRow {
135+
gap: 0.35rem;
136+
font-size: 0.85rem;
137+
}
138+
}

tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)