Skip to content

Commit fb36eff

Browse files
authored
Add compact meta bar and colored tag pills to blog content page
Agent-Logs-Url: https://github.com/recodehive/recode-website/sessions/63eb930b-2246-44a6-bb36-24cff4302f3a
1 parent 8737c84 commit fb36eff

3 files changed

Lines changed: 306 additions & 1 deletion

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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+
const primaryAuthor = metadata.authors?.[0];
66+
const profile = primaryAuthor?.key ? getAuthorProfile(primaryAuthor.key) : undefined;
67+
68+
const authorAvatar = primaryAuthor?.imageURL || profile?.imageUrl;
69+
const authorName = primaryAuthor?.name || profile?.name;
70+
const githubHandle = primaryAuthor
71+
? getGitHubHandle({ key: primaryAuthor.key, url: primaryAuthor.url })
72+
: undefined;
73+
const githubUrl = primaryAuthor
74+
? getGitHubUrl({ key: primaryAuthor.key, url: primaryAuthor.url, name: primaryAuthor.name })
75+
: undefined;
76+
77+
const roundedReadTime = Math.max(1, Math.ceil(metadata.readingTime || 0));
78+
const readTimeText = `${roundedReadTime} min read`;
79+
80+
const blogDate = metadata.date
81+
? new Intl.DateTimeFormat(siteConfig.i18n?.defaultLocale || "en-US", {
82+
year: "numeric",
83+
month: "short",
84+
day: "numeric",
85+
}).format(new Date(metadata.date))
86+
: undefined;
87+
88+
const tags = (metadata.tags ?? [])
89+
.map((t) => (typeof t === "string" ? t : (t as { label?: string }).label ?? ""))
90+
.filter(Boolean);
91+
92+
return (
93+
<>
94+
{/* Render only the title — Info and Authors are replaced by our compact bar */}
95+
<BlogPostItemHeaderTitle />
96+
<div className={styles.metaSection}>
97+
{/* Compact meta row: avatar · @handle · date · reading time */}
98+
<div className={styles.metaRow}>
99+
{(authorAvatar || authorName) && (
100+
<div className={styles.authorPart}>
101+
{authorAvatar ? (
102+
<img
103+
className={styles.avatar}
104+
src={authorAvatar}
105+
alt={authorName ? `${authorName} avatar` : "Author avatar"}
106+
loading="lazy"
107+
/>
108+
) : (
109+
<div className={styles.avatarFallback} aria-hidden="true">
110+
{authorName?.charAt(0).toUpperCase()}
111+
</div>
112+
)}
113+
{githubHandle &&
114+
(githubUrl ? (
115+
<Link
116+
to={githubUrl}
117+
className={styles.handle}
118+
target="_blank"
119+
rel="noopener noreferrer"
120+
>
121+
{githubHandle}
122+
</Link>
123+
) : (
124+
<span className={styles.handle}>{githubHandle}</span>
125+
))}
126+
</div>
127+
)}
128+
129+
{blogDate && (
130+
<>
131+
<span className={styles.sep} aria-hidden="true">
132+
133+
</span>
134+
<span className={styles.date}>{blogDate}</span>
135+
</>
136+
)}
137+
138+
<span className={styles.sep} aria-hidden="true">
139+
140+
</span>
141+
<span className={styles.readTime}>
142+
<svg
143+
xmlns="http://www.w3.org/2000/svg"
144+
viewBox="0 0 24 24"
145+
width="13"
146+
height="13"
147+
fill="none"
148+
stroke="currentColor"
149+
strokeWidth="2"
150+
strokeLinecap="round"
151+
strokeLinejoin="round"
152+
aria-hidden="true"
153+
>
154+
<circle cx="12" cy="12" r="10" />
155+
<polyline points="12 6 12 12 16 14" />
156+
</svg>
157+
{readTimeText}
158+
</span>
159+
</div>
160+
161+
{/* Colored tag pills */}
162+
{tags.length > 0 && (
163+
<div className={styles.tagsRow} aria-label="Tags">
164+
{tags.map((tag) => {
165+
const c = tagColor(tag);
166+
return (
167+
<span
168+
key={tag}
169+
className={styles.tag}
170+
style={
171+
{
172+
"--tag-dot": c.dot,
173+
"--tag-border": c.border,
174+
"--tag-bg": c.bg,
175+
"--tag-text": c.text,
176+
} as React.CSSProperties
177+
}
178+
>
179+
<span className={styles.tagDot} aria-hidden="true" />
180+
{tag}
181+
</span>
182+
);
183+
})}
184+
</div>
185+
)}
186+
</div>
187+
</>
188+
);
189+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* Blog post page: compact meta bar + tag pills */
2+
3+
.metaSection {
4+
display: flex;
5+
flex-direction: column;
6+
gap: 0.65rem;
7+
margin-top: 0.75rem;
8+
margin-bottom: 1.25rem;
9+
padding-bottom: 1.25rem;
10+
border-bottom: 1px solid var(--ifm-color-emphasis-200);
11+
}
12+
13+
/* ── Meta row: avatar · @handle · date · reading time ── */
14+
.metaRow {
15+
display: flex;
16+
align-items: center;
17+
flex-wrap: wrap;
18+
gap: 0.45rem;
19+
font-size: 0.9rem;
20+
color: var(--ifm-color-emphasis-700);
21+
}
22+
23+
.authorPart {
24+
display: flex;
25+
align-items: center;
26+
gap: 0.45rem;
27+
}
28+
29+
.avatar,
30+
.avatarFallback {
31+
width: 32px;
32+
height: 32px;
33+
border-radius: 50%;
34+
flex-shrink: 0;
35+
}
36+
37+
.avatar {
38+
object-fit: cover;
39+
border: 1px solid var(--ifm-color-emphasis-300);
40+
}
41+
42+
.avatarFallback {
43+
display: grid;
44+
place-items: center;
45+
font-weight: 700;
46+
font-size: 0.8rem;
47+
background: var(--ifm-color-emphasis-200);
48+
color: var(--ifm-color-primary-darkest);
49+
}
50+
51+
.handle {
52+
font-weight: 600;
53+
color: var(--ifm-color-primary);
54+
text-decoration: none;
55+
}
56+
57+
.handle:hover {
58+
text-decoration: underline;
59+
}
60+
61+
.sep {
62+
color: var(--ifm-color-emphasis-400);
63+
font-size: 0.8rem;
64+
line-height: 1;
65+
}
66+
67+
.date {
68+
color: var(--ifm-color-emphasis-700);
69+
}
70+
71+
.readTime {
72+
display: inline-flex;
73+
align-items: center;
74+
gap: 0.3rem;
75+
color: var(--ifm-color-emphasis-700);
76+
}
77+
78+
/* ── Tag pills ── */
79+
.tagsRow {
80+
display: flex;
81+
flex-wrap: wrap;
82+
gap: 0.45rem;
83+
}
84+
85+
.tag {
86+
display: inline-flex;
87+
align-items: center;
88+
gap: 0.35rem;
89+
padding: 3px 10px 3px 8px;
90+
border-radius: 20px;
91+
font-size: 0.8rem;
92+
font-weight: 500;
93+
background: var(--tag-bg);
94+
color: var(--tag-text);
95+
border: 1px solid var(--tag-border);
96+
white-space: nowrap;
97+
}
98+
99+
.tagDot {
100+
width: 6px;
101+
height: 6px;
102+
border-radius: 50%;
103+
background: var(--tag-dot);
104+
flex-shrink: 0;
105+
}
106+
107+
[data-theme="dark"] .tag {
108+
opacity: 0.85;
109+
}
110+
111+
@media (max-width: 480px) {
112+
.metaRow {
113+
gap: 0.35rem;
114+
font-size: 0.85rem;
115+
}
116+
}

tsconfig.tsbuildinfo

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

0 commit comments

Comments
 (0)