diff --git a/packages/create-docusaurus/templates/shared/blog/authors.yml b/packages/create-docusaurus/templates/shared/blog/authors.yml index bcb29915635c..39734f65b9cf 100644 --- a/packages/create-docusaurus/templates/shared/blog/authors.yml +++ b/packages/create-docusaurus/templates/shared/blog/authors.yml @@ -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 diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/authorsMapFiles/authors.yml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/authorsMapFiles/authors.yml index ac09421385d9..1d0cb11cd6ea 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/authorsMapFiles/authors.yml +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/authorsMapFiles/authors.yml @@ -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 diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md index 10ba9373aa47..b8b206e3fb92 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md @@ -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] --- diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/authors.yml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/authors.yml index a704e9e9841b..c44d2ee68da6 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/authors.yml +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/blog/authors.yml @@ -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 diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts index bb55d7b4c4ad..851bf77fa2fd 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts @@ -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); + }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/authorsSocials.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/authorsSocials.test.ts new file mode 100644 index 000000000000..af408e3dedb0 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/authorsSocials.test.ts @@ -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); + }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/authors.ts b/packages/docusaurus-plugin-content-blog/src/authors.ts index c5fdad61ddfb..541521286e02 100644 --- a/packages/docusaurus-plugin-content-blog/src/authors.ts +++ b/packages/docusaurus-plugin-content-blog/src/authors.ts @@ -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() .pattern( Joi.string(), - Joi.object({ + Joi.object({ 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 { - 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); diff --git a/packages/docusaurus-plugin-content-blog/src/authorsSocials.ts b/packages/docusaurus-plugin-content-blog/src/authorsSocials.ts new file mode 100644 index 000000000000..8ca12169a3b0 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/authorsSocials.ts @@ -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({ + 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)); +}; diff --git a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts index 3c3f0f8883ee..6c3436198038 100644 --- a/packages/docusaurus-plugin-content-blog/src/frontMatter.ts +++ b/packages/docusaurus-plugin-content-blog/src/frontMatter.ts @@ -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}); diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index 05985a667d7b..589a5e93b169 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -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> & { + /** + * Unknown keys are allowed: users can pass additional social platforms + */ + [customAuthorSocialPlatform: string]: string; + }; + export type Author = { key?: string; // TODO temporary, need refactor @@ -69,11 +92,15 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the * to generate a fallback `mailto:` URL. */ email?: string; + /** + * Social platforms of the author + * Usually displayed as a list of social icon links. + */ + socials?: AuthorSocials; /** * Unknown keys are allowed, so that we can pass custom fields to authors, - * e.g., `twitter`. */ - [key: string]: unknown; + [customAuthorAttribute: string]: unknown; }; /** diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index f3761d8857cb..1b0a871197e2 100644 --- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts @@ -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', diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index a71dfac8566d..142a229862f9 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -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'; diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/Socials/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/Socials/index.tsx new file mode 100644 index 000000000000..55866b14f2c2 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/Socials/index.tsx @@ -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 = { + 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 ( + + + + ); +} + +export default function AuthorSocials({author}: {author: Props['author']}) { + return ( +
+ {Object.entries(author.socials ?? {}).map(([platform, linkUrl]) => { + return ; + })} +
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/Socials/styles.module.css b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/Socials/styles.module.css new file mode 100644 index 000000000000..1fca8b7e385e --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/Socials/styles.module.css @@ -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); +} diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx index 5d9935febb04..0e81c7a42675 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx @@ -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 ( + + {title} + + ); +} + 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 (
{imageURL && ( @@ -32,14 +46,15 @@ export default function BlogPostItemHeaderAuthor({ )} - {name && ( + {(name || title) && (
- {name} + {name}
- {title && {title}} + {!!title && } + {hasSocials && }
)}
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/styles.module.css b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/styles.module.css new file mode 100644 index 000000000000..21ea5d40dc89 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/styles.module.css @@ -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; +} diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Authors/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Authors/index.tsx index d1ed5bb11cdd..9402f493b9e1 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Authors/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Authors/index.tsx @@ -25,6 +25,7 @@ export default function BlogPostItemHeaderAuthors({ return null; } const imageOnly = authors.every(({name}) => !name); + const singleAuthor = authors.length === 1; return (
(
): JSX.Element { + return ( + + + + + + + + + ); +} +export default DefaultSocial; diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/GitHub/index.tsx b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/GitHub/index.tsx new file mode 100644 index 000000000000..b20fd337c529 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/GitHub/index.tsx @@ -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): JSX.Element { + return ( + + + + ); +} +export default GitHub; diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/GitHub/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/GitHub/styles.module.css new file mode 100644 index 000000000000..716c6cc8efc3 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/GitHub/styles.module.css @@ -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); +} diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/LinkedIn/index.tsx b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/LinkedIn/index.tsx new file mode 100644 index 000000000000..b4b60dd47b5b --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/LinkedIn/index.tsx @@ -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): JSX.Element { + return ( + + + + ); +} +export default LinkedIn; diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/StackOverflow/index.tsx b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/StackOverflow/index.tsx new file mode 100644 index 000000000000..402bf16260dc --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/StackOverflow/index.tsx @@ -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): JSX.Element { + return ( + + + + + ); +} +export default StackOverflow; diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/Twitter/index.tsx b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/Twitter/index.tsx new file mode 100644 index 000000000000..5787de1ae241 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/Twitter/index.tsx @@ -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): JSX.Element { + return ( + + + + ); +} +export default Twitter; diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/X/index.tsx b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/X/index.tsx new file mode 100644 index 000000000000..7f066fd0d1f0 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/X/index.tsx @@ -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): JSX.Element { + return ( + + + + ); +} +export default X; diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/X/styles.module.css b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/X/styles.module.css new file mode 100644 index 000000000000..261dadc5b553 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/X/styles.module.css @@ -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); +} diff --git a/packages/docusaurus-utils/src/dataFileUtils.ts b/packages/docusaurus-utils/src/dataFileUtils.ts index 937d22b9cf2b..ba671d6e8875 100644 --- a/packages/docusaurus-utils/src/dataFileUtils.ts +++ b/packages/docusaurus-utils/src/dataFileUtils.ts @@ -63,6 +63,7 @@ export async function getDataFileData( 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.`; diff --git a/project-words.txt b/project-words.txt index 8b8a8f463f28..b9db52dfb400 100644 --- a/project-words.txt +++ b/project-words.txt @@ -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 diff --git a/website/_dogfooding/_blog tests/2024-07-03-dual-author.mdx b/website/_dogfooding/_blog tests/2024-07-03-dual-author.mdx new file mode 100644 index 000000000000..0a7ff12b993b --- /dev/null +++ b/website/_dogfooding/_blog tests/2024-07-03-dual-author.mdx @@ -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 diff --git a/website/_dogfooding/_blog tests/2024-07-03-multiple-authors.mdx b/website/_dogfooding/_blog tests/2024-07-03-multiple-authors.mdx new file mode 100644 index 000000000000..dcc74039e51a --- /dev/null +++ b/website/_dogfooding/_blog tests/2024-07-03-multiple-authors.mdx @@ -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 diff --git a/website/_dogfooding/_blog tests/2024-07-03-single-author.mdx b/website/_dogfooding/_blog tests/2024-07-03-single-author.mdx new file mode 100644 index 000000000000..0a418f42e283 --- /dev/null +++ b/website/_dogfooding/_blog tests/2024-07-03-single-author.mdx @@ -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 diff --git a/website/_dogfooding/dogfooding.config.ts b/website/_dogfooding/dogfooding.config.ts index e05203a191eb..f68020dca475 100644 --- a/website/_dogfooding/dogfooding.config.ts +++ b/website/_dogfooding/dogfooding.config.ts @@ -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', diff --git a/website/blog/authors.yml b/website/blog/authors.yml index 7fb45d8003ec..318effc9b46b 100644 --- a/website/blog/authors.yml +++ b/website/blog/authors.yml @@ -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 O’Shannessy 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 diff --git a/website/docs/api/plugins/plugin-content-blog.mdx b/website/docs/api/plugins/plugin-content-blog.mdx index c8931e4222c9..2eb10ccfb42c 100644 --- a/website/docs/api/plugins/plugin-content-blog.mdx +++ b/website/docs/api/plugins/plugin-content-blog.mdx @@ -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; + 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 diff --git a/website/docs/blog.mdx b/website/docs/blog.mdx index 75e4fa09a978..8ec62d11fea7 100644 --- a/website/docs/blog.mdx +++ b/website/docs/blog.mdx @@ -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