feat(blog): author header social icons (#10222)

Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
Co-authored-by: slorber <slorber@users.noreply.github.com>
This commit is contained in:
ozaki 2024-07-12 09:59:56 +02:00 committed by GitHub
parent 8b877d27d4
commit a6de0f2725
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1005 additions and 31 deletions

View file

@ -9,9 +9,17 @@ yangshun:
title: Front End Engineer @ Facebook
url: https://github.com/yangshun
image_url: https://github.com/yangshun.png
socials:
x: yangshunz
github: yangshun
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
socials:
x: sebastienlorber
linkedin: sebastienlorber
github: slorber
newsletter: https://thisweekinreact.com

View file

@ -4,15 +4,21 @@ JMarcey:
title: Technical Lead & Developer Advocate at Facebook
url: http://twitter.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
twitter: JoelMarcey
socials:
twitter: https://twitter.com/JoelMarcey
x: https://x.com/JoelMarcey
stackoverflow: https://stackoverflow.com/users/102705/Joel-Marcey
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
twitter: sebastienlorber
email: lorber.sebastien@gmail.com
socials:
twitter: sebastienlorber
x: sebastienlorber
stackoverflow: 82609
yangshun:
name: Yangshun Tay

View file

@ -2,6 +2,9 @@
title: Happy 1st Birthday Slash!
authors:
- name: Yangshun Tay
socials:
x: https://x.com/yangshunz
github: yangshun
- slorber
tags: [birthday,inlineTag,globalTag]
---

View file

@ -3,3 +3,7 @@ slorber:
title: Docusaurus maintainer
email: lorber.sebastien@gmail.com
url: https://sebastienlorber.com
socials:
twitter: sebastienlorber
x: https://x.com/sebastienlorber
github: slorber

View file

@ -255,6 +255,52 @@ describe('getBlogPostAuthors', () => {
]);
});
it('can normalize inline authors', () => {
expect(
getBlogPostAuthors({
frontMatter: {
authors: [
{
name: 'Seb1',
socials: {
x: 'https://x.com/sebastienlorber',
twitter: 'sebastienlorber',
github: 'slorber',
},
},
{
name: 'Seb2',
socials: {
x: 'sebastienlorber',
twitter: 'https://twitter.com/sebastienlorber',
github: 'https://github.com/slorber',
},
},
],
},
authorsMap: {},
baseUrl: '/',
}),
).toEqual([
{
name: 'Seb1',
socials: {
x: 'https://x.com/sebastienlorber',
twitter: 'https://twitter.com/sebastienlorber',
github: 'https://github.com/slorber',
},
},
{
name: 'Seb2',
socials: {
x: 'https://x.com/sebastienlorber',
twitter: 'https://twitter.com/sebastienlorber',
github: 'https://github.com/slorber',
},
},
]);
});
it('throw when using author key with no authorsMap', () => {
expect(() =>
getBlogPostAuthors({
@ -412,6 +458,29 @@ describe('getAuthorsMap', () => {
}),
).resolves.toBeUndefined();
});
describe('getAuthorsMap returns normalized', () => {
it('socials', async () => {
const authorsMap = await getAuthorsMap({
contentPaths,
authorsMapPath: 'authors.yml',
});
expect(authorsMap.slorber.socials).toMatchInlineSnapshot(`
{
"stackoverflow": "https://stackoverflow.com/users/82609",
"twitter": "https://twitter.com/sebastienlorber",
"x": "https://x.com/sebastienlorber",
}
`);
expect(authorsMap.JMarcey.socials).toMatchInlineSnapshot(`
{
"stackoverflow": "https://stackoverflow.com/users/102705/Joel-Marcey",
"twitter": "https://twitter.com/JoelMarcey",
"x": "https://x.com/JoelMarcey",
}
`);
});
});
});
describe('validateAuthorsMap', () => {
@ -529,3 +598,68 @@ describe('validateAuthorsMap', () => {
);
});
});
describe('authors socials', () => {
it('valid known author map socials', () => {
const authorsMap: AuthorsMap = {
ozaki: {
name: 'ozaki',
socials: {
twitter: 'ozakione',
github: 'ozakione',
},
},
};
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
});
it('throw socials that are not strings', () => {
const authorsMap: AuthorsMap = {
ozaki: {
name: 'ozaki',
socials: {
// @ts-expect-error: for tests
twitter: 42,
},
},
};
expect(() =>
validateAuthorsMap(authorsMap),
).toThrowErrorMatchingInlineSnapshot(
`""ozaki.socials.twitter" must be a string"`,
);
});
it('throw socials that are objects', () => {
const authorsMap: AuthorsMap = {
ozaki: {
name: 'ozaki',
socials: {
// @ts-expect-error: for tests
twitter: {link: 'ozakione'},
},
},
};
expect(() =>
validateAuthorsMap(authorsMap),
).toThrowErrorMatchingInlineSnapshot(
`""ozaki.socials.twitter" must be a string"`,
);
});
it('valid unknown author map socials', () => {
const authorsMap: AuthorsMap = {
ozaki: {
name: 'ozaki',
socials: {
random: 'ozakione',
},
},
};
expect(validateAuthorsMap(authorsMap)).toEqual(authorsMap);
});
});

View file

@ -0,0 +1,116 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {normalizeSocials} from '../authorsSocials';
import type {AuthorSocials} from '@docusaurus/plugin-content-blog';
describe('normalizeSocials', () => {
it('only username', () => {
const socials: AuthorSocials = {
twitter: 'ozakione',
linkedin: 'ozakione',
github: 'ozakione',
stackoverflow: 'ozakione',
};
expect(normalizeSocials(socials)).toMatchInlineSnapshot(`
{
"github": "https://github.com/ozakione",
"linkedin": "https://www.linkedin.com/in/ozakione/",
"stackoverflow": "https://stackoverflow.com/users/ozakione",
"twitter": "https://twitter.com/ozakione",
}
`);
});
it('only username - case insensitive', () => {
const socials: AuthorSocials = {
Twitter: 'ozakione',
linkedIn: 'ozakione',
gitHub: 'ozakione',
STACKoverflow: 'ozakione',
};
expect(normalizeSocials(socials)).toMatchInlineSnapshot(`
{
"github": "https://github.com/ozakione",
"linkedin": "https://www.linkedin.com/in/ozakione/",
"stackoverflow": "https://stackoverflow.com/users/ozakione",
"twitter": "https://twitter.com/ozakione",
}
`);
});
it('only links', () => {
const socials: AuthorSocials = {
twitter: 'https://x.com/ozakione',
linkedin: 'https://linkedin.com/ozakione',
github: 'https://github.com/ozakione',
stackoverflow: 'https://stackoverflow.com/ozakione',
};
expect(normalizeSocials(socials)).toEqual(socials);
});
it('mixed links', () => {
const socials: AuthorSocials = {
twitter: 'ozakione',
linkedin: 'ozakione',
github: 'https://github.com/ozakione',
stackoverflow: 'https://stackoverflow.com/ozakione',
};
expect(normalizeSocials(socials)).toMatchInlineSnapshot(`
{
"github": "https://github.com/ozakione",
"linkedin": "https://www.linkedin.com/in/ozakione/",
"stackoverflow": "https://stackoverflow.com/ozakione",
"twitter": "https://twitter.com/ozakione",
}
`);
});
it('one link', () => {
const socials: AuthorSocials = {
twitter: 'ozakione',
};
expect(normalizeSocials(socials)).toMatchInlineSnapshot(`
{
"twitter": "https://twitter.com/ozakione",
}
`);
});
it('rejects strings that do not look like username/userId/handle or fully-qualified URLs', () => {
const socials: AuthorSocials = {
twitter: '/ozakione/XYZ',
};
expect(() => normalizeSocials(socials)).toThrowErrorMatchingInlineSnapshot(`
"Author socials should be usernames/userIds/handles, or fully qualified HTTP(s) absolute URLs.
Social platform 'twitter' has illegal value '/ozakione/XYZ'"
`);
});
it('allow other form of urls', () => {
const socials: AuthorSocials = {
twitter: 'https://bit.ly/sebastienlorber-twitter',
};
expect(normalizeSocials(socials)).toEqual(socials);
});
it('allow unknown social platforms urls', () => {
const socials: AuthorSocials = {
twitch: 'https://www.twitch.tv/sebastienlorber',
newsletter: 'https://thisweekinreact.com',
};
expect(normalizeSocials(socials)).toEqual(socials);
});
});

View file

@ -5,8 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import * as _ from 'lodash';
import {getDataFileData, normalizeUrl} from '@docusaurus/utils';
import {Joi, URISchema} from '@docusaurus/utils-validation';
import {AuthorSocialsSchema, normalizeSocials} from './authorsSocials';
import type {BlogContentPaths} from './types';
import type {
Author,
@ -20,12 +22,13 @@ export type AuthorsMap = {[authorKey: string]: Author};
const AuthorsMapSchema = Joi.object<AuthorsMap>()
.pattern(
Joi.string(),
Joi.object({
Joi.object<Author>({
name: Joi.string(),
url: URISchema,
imageURL: URISchema,
title: Joi.string(),
email: Joi.string(),
socials: AuthorSocialsSchema,
})
.rename('image_url', 'imageURL')
.or('name', 'imageURL')
@ -51,18 +54,32 @@ export function validateAuthorsMap(content: unknown): AuthorsMap {
return value;
}
function normalizeAuthor(author: Author): Author {
return {
...author,
socials: author.socials ? normalizeSocials(author.socials) : undefined,
};
}
function normalizeAuthorsMap(authorsMap: AuthorsMap): AuthorsMap {
return _.mapValues(authorsMap, normalizeAuthor);
}
export async function getAuthorsMap(params: {
authorsMapPath: string;
contentPaths: BlogContentPaths;
}): Promise<AuthorsMap | undefined> {
return getDataFileData(
const authorsMap = await getDataFileData(
{
filePath: params.authorsMapPath,
contentPaths: params.contentPaths,
fileType: 'authors map',
},
// TODO annoying to test: tightly coupled FS reads + validation...
validateAuthorsMap,
);
return authorsMap ? normalizeAuthorsMap(authorsMap) : undefined;
}
type AuthorsParam = {
@ -115,7 +132,7 @@ function getFrontMatterAuthorLegacy({
function normalizeFrontMatterAuthors(
frontMatterAuthors: BlogPostFrontMatterAuthors = [],
): BlogPostFrontMatterAuthor[] {
function normalizeAuthor(
function normalizeFrontMatterAuthor(
authorInput: string | Author,
): BlogPostFrontMatterAuthor {
if (typeof authorInput === 'string') {
@ -128,8 +145,8 @@ function normalizeFrontMatterAuthors(
}
return Array.isArray(frontMatterAuthors)
? frontMatterAuthors.map(normalizeAuthor)
: [normalizeAuthor(frontMatterAuthors)];
? frontMatterAuthors.map(normalizeFrontMatterAuthor)
: [normalizeFrontMatterAuthor(frontMatterAuthors)];
}
function getFrontMatterAuthors(params: AuthorsParam): Author[] {
@ -158,11 +175,11 @@ ${Object.keys(authorsMap)
}
function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
return {
return normalizeAuthor({
// Author def from authorsMap can be locally overridden by front matter
...getAuthorsMapAuthor(frontMatterAuthor.key),
...frontMatterAuthor,
};
});
}
return frontMatterAuthors.map(toAuthor);

View file

@ -0,0 +1,64 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Joi} from '@docusaurus/utils-validation';
import type {
AuthorSocials,
SocialPlatformKey,
} from '@docusaurus/plugin-content-blog';
export const AuthorSocialsSchema = Joi.object<AuthorSocials>({
twitter: Joi.string(),
github: Joi.string(),
linkedin: Joi.string(),
// StackOverflow userIds like '82609' are parsed as numbers by Yaml
stackoverflow: Joi.alternatives()
.try(Joi.number(), Joi.string())
.custom((val) => String(val)),
x: Joi.string(),
}).unknown();
type PredefinedPlatformNormalizer = (value: string) => string;
const PredefinedPlatformNormalizers: Record<
SocialPlatformKey | string,
PredefinedPlatformNormalizer
> = {
x: (handle: string) => `https://x.com/${handle}`,
twitter: (handle: string) => `https://twitter.com/${handle}`,
github: (handle: string) => `https://github.com/${handle}`,
linkedin: (handle: string) => `https://www.linkedin.com/in/${handle}/`,
stackoverflow: (userId: string) =>
`https://stackoverflow.com/users/${userId}`,
};
type SocialEntry = [string, string];
function normalizeSocialEntry([platform, value]: SocialEntry): SocialEntry {
const normalizer = PredefinedPlatformNormalizers[platform.toLowerCase()];
const isAbsoluteUrl =
value.startsWith('http://') || value.startsWith('https://');
if (isAbsoluteUrl) {
return [platform, value];
} else if (value.includes('/')) {
throw new Error(
`Author socials should be usernames/userIds/handles, or fully qualified HTTP(s) absolute URLs.
Social platform '${platform}' has illegal value '${value}'`,
);
}
if (normalizer && !isAbsoluteUrl) {
const normalizedPlatform = platform.toLowerCase();
const normalizedValue = normalizer(value);
return [normalizedPlatform as SocialPlatformKey, normalizedValue];
}
return [platform, value];
}
export const normalizeSocials = (socials: AuthorSocials): AuthorSocials => {
return Object.fromEntries(Object.entries(socials).map(normalizeSocialEntry));
};

View file

@ -13,6 +13,7 @@ import {
URISchema,
validateFrontMatter,
} from '@docusaurus/utils-validation';
import {AuthorSocialsSchema} from './authorsSocials';
import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';
const BlogPostFrontMatterAuthorSchema = Joi.object({
@ -21,6 +22,7 @@ const BlogPostFrontMatterAuthorSchema = Joi.object({
title: Joi.string(),
url: URISchema,
imageURL: Joi.string(),
socials: AuthorSocialsSchema,
})
.or('key', 'name', 'imageURL')
.rename('image_url', 'imageURL', {alias: true});

View file

@ -43,6 +43,29 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
authorsImageUrls: (string | undefined)[];
};
/**
* Note we don't pre-define all possible platforms
* Users can add their own custom platforms if needed
*/
export type SocialPlatformKey =
| 'twitter'
| 'github'
| 'linkedin'
| 'stackoverflow'
| 'x';
/**
* Social platforms of the author.
* The record value is usually the fully qualified link of the social profile.
* For pre-defined platforms, it's possible to pass a handle instead
*/
export type AuthorSocials = Partial<Record<SocialPlatformKey, string>> & {
/**
* Unknown keys are allowed: users can pass additional social platforms
*/
[customAuthorSocialPlatform: string]: string;
};
export type Author = {
key?: string; // TODO temporary, need refactor
@ -70,10 +93,14 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
*/
email?: string;
/**
* Unknown keys are allowed, so that we can pass custom fields to authors,
* e.g., `twitter`.
* Social platforms of the author
* Usually displayed as a list of social icon links.
*/
[key: string]: unknown;
socials?: AuthorSocials;
/**
* Unknown keys are allowed, so that we can pass custom fields to authors,
*/
[customAuthorAttribute: string]: unknown;
};
/**

View file

@ -289,6 +289,17 @@ export default function getSwizzleConfig(): SwizzleConfig {
},
description: 'The menu icon component',
},
'Icon/Socials': {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
// TODO the CLI should rather support --eject
// Subfolders can be swizzled
eject: 'forbidden',
wrap: 'forbidden',
},
description:
'The Icon/Socials folder is not directly swizzle-able, but you can swizzle its sub-components.',
},
MDXComponents: {
actions: {
eject: 'safe',

View file

@ -292,16 +292,30 @@ declare module '@theme/BlogPostItem/Header/Info' {
}
declare module '@theme/BlogPostItem/Header/Author' {
import type {PropBlogPostContent} from '@docusaurus/plugin-content-blog';
import type {Author} from '@docusaurus/plugin-content-blog';
export interface Props {
readonly author: PropBlogPostContent['metadata']['authors'][number];
readonly author: Author;
readonly singleAuthor: boolean;
readonly className?: string;
}
export default function BlogPostItemHeaderAuthor(props: Props): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Author/Socials' {
import type {Author} from '@docusaurus/plugin-content-blog';
export interface Props {
readonly author: Author;
readonly className?: string;
}
export default function BlogPostItemHeaderAuthorSocials(
props: Props,
): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Authors' {
export interface Props {
readonly className?: string;
@ -1514,6 +1528,54 @@ declare module '@theme/Icon/WordWrap' {
export default function IconWordWrap(props: Props): JSX.Element;
}
declare module '@theme/Icon/Socials/Twitter' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function Twitter(props: Props): JSX.Element;
}
declare module '@theme/Icon/Socials/GitHub' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function Github(props: Props): JSX.Element;
}
declare module '@theme/Icon/Socials/X' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function X(props: Props): JSX.Element;
}
declare module '@theme/Icon/Socials/LinkedIn' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function LinkedIn(props: Props): JSX.Element;
}
declare module '@theme/Icon/Socials/Default' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function DefaultSocialIcon(props: Props): JSX.Element;
}
declare module '@theme/Icon/Socials/StackOverflow' {
import type {ComponentProps} from 'react';
export interface Props extends ComponentProps<'svg'> {}
export default function StackOverflow(props: Props): JSX.Element;
}
declare module '@theme/TagsListByLetter' {
import type {TagsListItem} from '@docusaurus/utils';

View file

@ -0,0 +1,61 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {ComponentType} from 'react';
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogPostItem/Header/Author/Socials';
import Twitter from '@theme/Icon/Socials/Twitter';
import GitHub from '@theme/Icon/Socials/GitHub';
import X from '@theme/Icon/Socials/X';
import StackOverflow from '@theme/Icon/Socials/StackOverflow';
import LinkedIn from '@theme/Icon/Socials/LinkedIn';
import DefaultSocialIcon from '@theme/Icon/Socials/Default';
import styles from './styles.module.css';
type SocialIcon = ComponentType<{className: string}>;
type SocialPlatformConfig = {Icon: SocialIcon; label: string};
const SocialPlatformConfigs: Record<string, SocialPlatformConfig> = {
twitter: {Icon: Twitter, label: 'Twitter'},
github: {Icon: GitHub, label: 'GitHub'},
stackoverflow: {Icon: StackOverflow, label: 'Stack Overflow'},
linkedin: {Icon: LinkedIn, label: 'LinkedIn'},
x: {Icon: X, label: 'X'},
};
function getSocialPlatformConfig(platformKey: string): SocialPlatformConfig {
return (
SocialPlatformConfigs[platformKey] ?? {
Icon: DefaultSocialIcon,
label: platformKey,
}
);
}
function SocialLink({platform, link}: {platform: string; link: string}) {
const {Icon, label} = getSocialPlatformConfig(platform);
return (
<Link className={styles.authorSocialLink} href={link} title={label}>
<Icon className={clsx(styles.authorSocialLink)} />
</Link>
);
}
export default function AuthorSocials({author}: {author: Props['author']}) {
return (
<div className={styles.authorSocials}>
{Object.entries(author.socials ?? {}).map(([platform, linkUrl]) => {
return <SocialLink key={platform} platform={platform} link={linkUrl} />;
})}
</div>
);
}

View file

@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
:root {
--docusaurus-blog-social-icon-size: 1rem;
}
.authorSocials {
margin-top: 0.2rem;
display: flex;
flex-wrap: wrap;
align-items: center;
line-height: 0;
overflow: hidden;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.authorSocialLink {
height: var(--docusaurus-blog-social-icon-size);
width: var(--docusaurus-blog-social-icon-size);
line-height: 0;
margin-right: 0.3rem;
}
.authorSocialIcon {
width: var(--docusaurus-blog-social-icon-size);
height: var(--docusaurus-blog-social-icon-size);
}

View file

@ -8,8 +8,10 @@
import React from 'react';
import clsx from 'clsx';
import Link, {type Props as LinkProps} from '@docusaurus/Link';
import AuthorSocials from '@theme/BlogPostItem/Header/Author/Socials';
import type {Props} from '@theme/BlogPostItem/Header/Author';
import styles from './styles.module.css';
function MaybeLink(props: LinkProps): JSX.Element {
if (props.href) {
@ -18,12 +20,24 @@ function MaybeLink(props: LinkProps): JSX.Element {
return <>{props.children}</>;
}
function AuthorTitle({title}: {title: string}) {
return (
<small className={styles.authorTitle} title={title}>
{title}
</small>
);
}
export default function BlogPostItemHeaderAuthor({
// singleAuthor, // may be useful in the future, or for swizzle users
author,
className,
}: Props): JSX.Element {
const {name, title, url, imageURL, email} = author;
const {name, title, url, socials, imageURL, email} = author;
const link = url || (email && `mailto:${email}`) || undefined;
const hasSocials = socials && Object.keys(socials).length > 0;
return (
<div className={clsx('avatar margin-bottom--sm', className)}>
{imageURL && (
@ -32,14 +46,15 @@ export default function BlogPostItemHeaderAuthor({
</MaybeLink>
)}
{name && (
{(name || title) && (
<div className="avatar__intro">
<div className="avatar__name">
<MaybeLink href={link}>
<span>{name}</span>
<span className={styles.authorName}>{name}</span>
</MaybeLink>
</div>
{title && <small className="avatar__subtitle">{title}</small>}
{!!title && <AuthorTitle title={title} />}
{hasSocials && <AuthorSocials author={author} />}
</div>
)}
</div>

View file

@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.authorName {
font-size: 1.1rem;
}
.authorTitle {
margin-top: 0.06rem;
font-size: 0.8rem;
line-height: 0.8rem;
display: -webkit-box;
overflow: hidden;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}

View file

@ -25,6 +25,7 @@ export default function BlogPostItemHeaderAuthors({
return null;
}
const imageOnly = authors.every(({name}) => !name);
const singleAuthor = authors.length === 1;
return (
<div
className={clsx(
@ -35,11 +36,12 @@ export default function BlogPostItemHeaderAuthors({
{authors.map((author, idx) => (
<div
className={clsx(
!imageOnly && 'col col--6',
!imageOnly && (singleAuthor ? 'col col--12' : 'col col--6'),
imageOnly ? styles.imageOnlyAuthorCol : styles.authorCol,
)}
key={idx}>
<BlogPostItemHeaderAuthor
singleAuthor={singleAuthor}
author={{
...author,
// Handle author images using relative paths

View file

@ -7,7 +7,6 @@
.authorCol {
max-width: inherit !important;
flex-grow: 1 !important;
}
.imageOnlyAuthorRow {

View file

@ -0,0 +1,33 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {SVGProps} from 'react';
// SVG Source: https://tabler.io/
function DefaultSocial(props: SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M3.6 9h16.8" />
<path d="M3.6 15h16.8" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
);
}
export default DefaultSocial;

View file

@ -0,0 +1,29 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {SVGProps} from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
// SVG Source: https://svgl.app/
function GitHub(props: SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
viewBox="0 0 256 250"
width="1em"
height="1em"
{...props}
className={clsx(props.className, styles.githubSvg)}
xmlns="http://www.w3.org/2000/svg"
style={{'--dark': '#000', '--light': '#fff'} as React.CSSProperties}
preserveAspectRatio="xMidYMid">
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
</svg>
);
}
export default GitHub;

View file

@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
[data-theme='dark'] .githubSvg {
fill: var(--light);
}
[data-theme='light'] .githubSvg {
fill: var(--dark);
}

View file

@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {SVGProps} from 'react';
// SVG Source: https://svgl.app/
function LinkedIn(props: SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
width="1em"
height="1em"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
viewBox="0 0 256 256"
{...props}>
<path
d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453"
fill="#0A66C2"
/>
</svg>
);
}
export default LinkedIn;

View file

@ -0,0 +1,30 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {SVGProps} from 'react';
// SVG Source: https://svgl.app/
function StackOverflow(props: SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 169.61 200"
width="1em"
height="1em"
{...props}>
<path
d="M140.44 178.38v-48.65h21.61V200H0v-70.27h21.61v48.65z"
fill="#bcbbbb"
/>
<path
d="M124.24 140.54l4.32-16.22-86.97-17.83-3.78 17.83zM49.7 82.16L130.72 120l7.56-16.22-81.02-37.83zm22.68-40l68.06 57.3 11.35-13.51-68.6-57.3-11.35 13.51zM116.14 0l-14.59 10.81 53.48 71.89 14.58-10.81zM37.81 162.16h86.43v-16.21H37.81z"
fill="#f48024"
/>
</svg>
);
}
export default StackOverflow;

View file

@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {SVGProps} from 'react';
// SVG Source: https://svgl.app/
function Twitter(props: SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
viewBox="0 0 256 209"
width="1em"
height="1em"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
{...props}>
<path
d="M256 25.45c-9.42 4.177-19.542 7-30.166 8.27 10.845-6.5 19.172-16.793 23.093-29.057a105.183 105.183 0 0 1-33.351 12.745C205.995 7.201 192.346.822 177.239.822c-29.006 0-52.523 23.516-52.523 52.52 0 4.117.465 8.125 1.36 11.97-43.65-2.191-82.35-23.1-108.255-54.876-4.52 7.757-7.11 16.78-7.11 26.404 0 18.222 9.273 34.297 23.365 43.716a52.312 52.312 0 0 1-23.79-6.57c-.003.22-.003.44-.003.661 0 25.447 18.104 46.675 42.13 51.5a52.592 52.592 0 0 1-23.718.9c6.683 20.866 26.08 36.05 49.062 36.475-17.975 14.086-40.622 22.483-65.228 22.483-4.24 0-8.42-.249-12.529-.734 23.243 14.902 50.85 23.597 80.51 23.597 96.607 0 149.434-80.031 149.434-149.435 0-2.278-.05-4.543-.152-6.795A106.748 106.748 0 0 0 256 25.45"
fill="#55acee"
/>
</svg>
);
}
export default Twitter;

View file

@ -0,0 +1,29 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {SVGProps} from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
// SVG Source: https://svgl.app/
function X(props: SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 1200 1227"
{...props}
className={clsx(props.className, styles.xSvg)}
style={{'--dark': '#000', '--light': '#fff'} as React.CSSProperties}>
<path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
</svg>
);
}
export default X;

View file

@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
[data-theme='dark'] .xSvg {
fill: var(--light);
}
[data-theme='light'] .xSvg {
fill: var(--dark);
}

View file

@ -63,6 +63,7 @@ export async function getDataFileData<T>(
try {
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
const unsafeContent = Yaml.load(contentString);
// TODO we shouldn't validate here: it makes validation harder to test
return validate(unsafeContent);
} catch (err) {
logger.error`The ${params.fileType} file at path=${filePath} looks invalid.`;

View file

@ -145,6 +145,7 @@ javadoc
jiti
jmarcey
jodyheavener
joelmarcey
joshcena
jssdk
Kaszubowski
@ -235,6 +236,7 @@ outerbounds
Outerbounds
overrideable
ozaki
ozakione
pageview
palenight
Palenight
@ -330,6 +332,7 @@ Solana
spâce
stackblitz
stackblitzrc
stackoverflow
Stormkit
Strikethrough
strikethroughs

View file

@ -0,0 +1,26 @@
---
title: Dual author socials
authors:
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
socials:
twitter: sebastienlorber
github: slorber
stackoverflow: 82609
linkedin: sebastienlorber
newsletter: https://thisweekinreact.com/newsletter
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
socials:
x: https://x.com/sebastienlorber
github: https://github.com/slorber
stackoverflow: 82609
linkedin: https://www.linkedin.com/in/sebastienlorber/
newsletter: https://thisweekinreact.com/newsletter
---
# Multiple authors
## Content
Content about the blog post

View file

@ -0,0 +1,93 @@
---
title: How multiple authors with socials looks
authors:
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
title: Docusaurus Maintainer and This Week In React editor editor editor editor editor editor editor editor
socials:
twitter: https://twitter.com/sebastienlorber
x: https://x.com/sebastienlorber
github: https://github.com/slorber
stackoverflow: https://stackoverflow.com/users/82609/sebastien-lorber
linkedin: https://www.linkedin.com/in/sebastienlorber/
newsletter: https://thisweekinreact.com/newsletter
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
socials:
twitter: https://x.com/sebastienlorber
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
title: Docusaurus Maintainer and This Week In React editor
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
title: Docusaurus Maintainer and This Week In React editor editor editor editor editor editor editor editor
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
title: Docusaurus Maintainer and This Week In React editor
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
socials:
github: https://github.com/slorber
twitter: https://twitter.com/sebastienlorber
x: https://x.com/sebastienlorber
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
title: Docusaurus Maintainer and This Week In React editor editor editor editor editor editor editor editor
socials:
github: https://github.com/slorber
twitter: https://x.com/sebastienlorber
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
socials:
a: https://thisweekinreact.com/newsletter
b: https://thisweekinreact.com/newsletter
c: https://thisweekinreact.com/newsletter
d: https://thisweekinreact.com/newsletter
e: https://thisweekinreact.com/newsletter
f: https://thisweekinreact.com/newsletter
g: https://thisweekinreact.com/newsletter
h: https://thisweekinreact.com/newsletter
i: https://thisweekinreact.com/newsletter
j: https://thisweekinreact.com/newsletter
k: https://thisweekinreact.com/newsletter
l: https://thisweekinreact.com/newsletter
m: https://thisweekinreact.com/newsletter
n: https://thisweekinreact.com/newsletter
o: https://thisweekinreact.com/newsletter
p: https://thisweekinreact.com/newsletter
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
socials:
a: https://thisweekinreact.com/newsletter
b: https://thisweekinreact.com/newsletter
c: https://thisweekinreact.com/newsletter
d: https://thisweekinreact.com/newsletter
e: https://thisweekinreact.com/newsletter
f: https://thisweekinreact.com/newsletter
g: https://thisweekinreact.com/newsletter
h: https://thisweekinreact.com/newsletter
i: https://thisweekinreact.com/newsletter
j: https://thisweekinreact.com/newsletter
k: https://thisweekinreact.com/newsletter
l: https://thisweekinreact.com/newsletter
m: https://thisweekinreact.com/newsletter
n: https://thisweekinreact.com/newsletter
o: https://thisweekinreact.com/newsletter
p: https://thisweekinreact.com/newsletter
q: https://thisweekinreact.com/newsletter
r: https://thisweekinreact.com/newsletter
s: https://thisweekinreact.com/newsletter
t: https://thisweekinreact.com/newsletter
u: https://thisweekinreact.com/newsletter
v: https://thisweekinreact.com/newsletter
w: https://thisweekinreact.com/newsletter
x: https://thisweekinreact.com/newsletter
y: https://thisweekinreact.com/newsletter
z: https://thisweekinreact.com/newsletter
---
# Multiple authors
## Content
Content about the blog post

View file

@ -0,0 +1,20 @@
---
title: Single author socials
authors:
- name: Sébastien Lorber
imageURL: https://github.com/slorber.png
title: Docusaurus Maintainer and This Week In React editor editor editor editor editor editor editor editor editor editor editor editor editor editor
socials:
x: https://x.com/sebastienlorber
twitter: https://twitter.com/sebastienlorber
github: https://github.com/slorber
stackoverflow: 82609
linkedin: https://www.linkedin.com/in/sebastienlorber/
newsletter: https://thisweekinreact.com/newsletter
---
# Multiple authors
## Content
Content about the blog post

View file

@ -83,6 +83,7 @@ export const dogfoodingPluginInstances: PluginConfig[] = [
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/_dogfooding/_blog-tests',
postsPerPage: 3,
blogSidebarCount: 'ALL',
feedOptions: {
type: 'all',
title: 'Docusaurus Tests Blog',

View file

@ -4,31 +4,39 @@ JMarcey:
url: https://twitter.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
email: jimarcey@gmail.com
twitter: JoelMarcey
socials:
x: joelmarcey
github: JoelMarcey
zpao:
name: Paul OShannessy
title: Engineering Manager at Meta
url: https://twitter.com/zpao
url: https://x.com/zpao
image_url: https://github.com/zpao.png
email: jimarcey@gmail.com
twitter: zpao
socials:
x: zpao
github: zpao
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer, This Week In React editor
url: https://thisweekinreact.com
image_url: https://github.com/slorber.png
twitter: sebastienlorber
email: sebastien@thisweekinreact.com
socials:
x: sebastienlorber
linkedin: sebastienlorber
github: slorber
newsletter: https://thisweekinreact.com
yangshun:
name: Yangshun Tay
title: Front End Engineer at Meta
url: https://github.com/yangshun
image_url: https://github.com/yangshun.png
twitter: yangshunz
email: tay.yang.shun@gmail.com
socials:
x: yangshunz
github: yangshun
lex111:
name: Alexey Pyltsyn
@ -49,17 +57,19 @@ endiliey:
title: Maintainer of Docusaurus
url: https://github.com/endiliey
image_url: https://github.com/endiliey.png
twitter: endiliey
abernathyca:
name: Christine Abernathy
url: http://twitter.com/abernathyca
url: http://x.com/abernathyca
image_url: https://github.com/caabernathy.png
twitter: abernathyca
socials:
x: abernathyca
shortcuts:
name: Clément Vannicatte
title: Software Engineer @ Algolia
url: https://github.com/shortcuts
image_url: https://github.com/shortcuts.png
twitter: sh0rtcts
socials:
x: sh0rtcts
github: shortcuts

View file

@ -251,12 +251,19 @@ type Tag = string | {label: string; permalink: string};
// An author key references an author from the global plugin authors.yml file
type AuthorKey = string;
// Social platform name -> Social platform link
// Example: {MyPlatform: 'https://myplatform.com/myusername'}
// Pre-defined platforms ("x", "github", "twitter", "linkedin", "stackoverflow") accept handles:
// Example: {github: 'slorber'}
type AuthorSocials = Record<string, string>;
type Author = {
key?: AuthorKey;
name: string;
title?: string;
url?: string;
image_url?: string;
socials?: AuthorSocials;
};
// The front matter authors field allows various possible shapes
@ -275,6 +282,9 @@ authors:
title: Co-creator of Docusaurus 1
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
socials:
x: joelmarcey
github: JoelMarcey
tags: [docusaurus]
description: This is my first post on Docusaurus.
image: https://i.imgur.com/mErPwqL.png

View file

@ -52,10 +52,16 @@ authors:
title: Co-creator of Docusaurus 1
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
socials:
x: joelmarcey
github: JoelMarcey
- name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
socials:
x: sebastienlorber
github: slorber
tags: [hello, docusaurus-v2]
image: https://i.imgur.com/mErPwqL.png
hide_table_of_contents: false
@ -214,6 +220,9 @@ authors:
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
email: jimarcey@gmail.com
socials:
x: joelmarcey
github: JoelMarcey
---
```
@ -230,10 +239,16 @@ authors:
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
email: jimarcey@gmail.com
socials:
x: joelmarcey
github: JoelMarcey
- name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
socials:
x: sebastienlorber
github: slorber
---
```
@ -276,12 +291,18 @@ jmarcey:
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
email: jimarcey@gmail.com
socials:
x: joelmarcey
github: JoelMarcey
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
socials:
x: sebastienlorber
github: slorber
```
:::tip