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..6b689a2fbee3 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/authorsSocials.test.ts @@ -0,0 +1,99 @@ +/** + * 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/ozakione", + "stackoverflow": "https://stackoverflow.com/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/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('normalize link url', () => { + // stackoverflow doesn't like multiple slashes, as we have trailing slash + // in socialPlatforms, if the user add a prefix slash the url will contain + // multiple slashes causing a 404 + const socials: AuthorSocials = { + twitter: '//ozakione', + github: '/ozakione///', + linkedin: '//ozakione///', + stackoverflow: '///users///82609/sebastien-lorber', + }; + + expect(normalizeSocials(socials)).toMatchInlineSnapshot(` + { + "github": "https://github.com/ozakione/", + "linkedin": "https://www.linkedin.com/ozakione/", + "stackoverflow": "https://stackoverflow.com/users/82609/sebastien-lorber", + "twitter": "https://twitter.com/ozakione", + } + `); + }); + + it('allow other form of urls', () => { + const socials: AuthorSocials = { + twitter: 'https://bit.ly/sebastienlorber-twitter', + }; + + 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..61d1be97d4de 100644 --- a/packages/docusaurus-plugin-content-blog/src/authors.ts +++ b/packages/docusaurus-plugin-content-blog/src/authors.ts @@ -26,6 +26,12 @@ const AuthorsMapSchema = Joi.object() imageURL: URISchema, title: Joi.string(), email: Joi.string(), + socials: Joi.object({ + twitter: Joi.string(), + github: Joi.string(), + linkedin: Joi.string(), + stackoverflow: Joi.string(), + }).unknown(), }) .rename('image_url', 'imageURL') .or('name', 'imageURL') 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..ec379cd5eb0e --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/authorsSocials.ts @@ -0,0 +1,35 @@ +/** + * 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 {normalizeUrl} from '@docusaurus/utils'; +import type { + AuthorSocials, + SocialPlatform, +} from '@docusaurus/plugin-content-blog'; + +const socialPlatforms: Record = { + twitter: 'https://twitter.com/', + github: 'https://github.com/', + linkedin: 'https://www.linkedin.com/', + stackoverflow: 'https://stackoverflow.com/', +}; + +export const normalizeSocials = (value: AuthorSocials): AuthorSocials => { + (Object.keys(socialPlatforms) as SocialPlatform[]).forEach((platform) => { + if ( + value[platform] && + !value[platform]!.startsWith(socialPlatforms[platform]) + ) { + value[platform] = normalizeUrl([ + socialPlatforms[platform], + value[platform]!, + ]); + } + }); + + return value; +}; diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 68f429cc8c0c..3e3d2994e66f 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -31,6 +31,7 @@ import {getTagsFile} from '@docusaurus/utils-validation'; import {validateBlogPostFrontMatter} from './frontMatter'; import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors'; import {reportAuthorsProblems} from './authorsProblems'; +import {normalizeSocials} from './authorsSocials'; import type {TagsFile} from '@docusaurus/utils'; import type {LoadContext, ParseFrontMatter} from '@docusaurus/types'; import type { @@ -383,6 +384,13 @@ export async function generateBlogPosts( authorsMapPath: options.authorsMapPath, }); + if (authorsMap) { + Object.entries(authorsMap).forEach(([, author]) => { + if (author.socials) { + author.socials = normalizeSocials(author.socials); + } + }); + } const tagsFile = await getTagsFile({contentPaths, tags: options.tags}); async function doProcessBlogSourceFile(blogSourceFile: string) { 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..75437019a569 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,10 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the authorsImageUrls: (string | undefined)[]; }; + type SocialPlatform = 'twitter' | 'github' | 'linkedin' | 'stackoverflow'; + + export type AuthorSocials = Partial>; + export type Author = { key?: string; // TODO temporary, need refactor @@ -70,10 +74,12 @@ 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`. + * TODO write a description */ - [key: string]: unknown; + socials?: Partial> & { + [customAuthorSocialPlatform: string]: string; + }; + [customAuthorAttribute: string]: unknown; }; /** diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index a71dfac8566d..36ff2599cf77 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -1514,6 +1514,46 @@ 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/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 DefaultSocial(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/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx index 5d9935febb04..9223ff83746e 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 @@ -5,11 +5,18 @@ * 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, {type Props as LinkProps} from '@docusaurus/Link'; import type {Props} from '@theme/BlogPostItem/Header/Author'; +import Twitter from '@theme/Icon/Socials/Twitter'; +import Github from '@theme/Icon/Socials/Github'; +import StackOverflow from '@theme/Icon/Socials/StackOverflow'; +import LinkedIn from '@theme/Icon/Socials/LinkedIn'; +import DefaultSocial from '@theme/Icon/Socials/Default'; +import styles from './styles.module.css'; function MaybeLink(props: LinkProps): JSX.Element { if (props.href) { @@ -18,12 +25,41 @@ function MaybeLink(props: LinkProps): JSX.Element { return <>{props.children}; } +const PlatformIconsMap: Record> = { + twitter: Twitter, + github: Github, + stackoverflow: StackOverflow, + linkedin: LinkedIn, +}; + +function PlatformLink({platform, link}: {platform: string; link: string}) { + const Icon = PlatformIconsMap[platform] ?? DefaultSocial; + return ( + + + + ); +} + export default function BlogPostItemHeaderAuthor({ 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 renderSocialMedia = () => ( +
+ {Object.entries(socials ?? {}).map(([platform, linkUrl]) => { + return ( + + ); + })} +
+ ); + + const hasSocialMedia = socials && Object.keys(socials).length > 0; + return (
{imageURL && ( @@ -39,7 +75,11 @@ export default function BlogPostItemHeaderAuthor({ {name}
- {title && {title}} + {hasSocialMedia ? ( + renderSocialMedia() + ) : ( + {title} + )} )} 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..7a56000247e3 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/styles.module.css @@ -0,0 +1,18 @@ +/** + * 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. + */ + +.authorSocial { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.socialIcon { + width: 1.375em; + height: 1.375em; + margin-right: 0.5em; +} diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/Socials/Default/index.tsx b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/Default/index.tsx new file mode 100644 index 000000000000..c5a15cd67d90 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/Icon/Socials/Default/index.tsx @@ -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): 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..4551d3bad48b --- /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/project-words.txt b/project-words.txt index 8b8a8f463f28..cc65e25d344e 100644 --- a/project-words.txt +++ b/project-words.txt @@ -166,6 +166,7 @@ linkify Linkify Localizable lockb +lorber Lorber Lorber's lqip @@ -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..d00f93b68715 --- /dev/null +++ b/website/_dogfooding/_blog tests/2024-07-03-dual-author.mdx @@ -0,0 +1,26 @@ +--- +title: Dual author socials +authors: + - name: slorber + imageURL: https://github.com/slorber.png + socials: + twitter: sebastienlorber + github: slorber + stackoverflow: https://stackoverflow.com/users/82609/sebastien-lorber + linkedin: https://www.linkedin.com/in/sebastienlorber/ + newsletter: https://thisweekinreact.com/newsletter + - name: slorber + imageURL: https://github.com/slorber.png + socials: + twitter: sebastienlorber + github: slorber + stackoverflow: https://stackoverflow.com/users/82609/sebastien-lorber + 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..ff43e0893268 --- /dev/null +++ b/website/_dogfooding/_blog tests/2024-07-03-multiple-authors.mdx @@ -0,0 +1,71 @@ +--- +title: How multiple authors with socials looks +authors: + - name: slorber + imageURL: https://github.com/slorber.png + socials: + twitter: sebastienlorber + github: slorber + stackoverflow: https://stackoverflow.com/users/82609/sebastien-lorber + linkedin: https://www.linkedin.com/in/sebastienlorber/ + newsletter: https://thisweekinreact.com/newsletter + - name: slorber + imageURL: https://github.com/slorber.png + socials: + twitter: sebastienlorber + - name: slorber + imageURL: https://github.com/slorber.png + title: Docusaurus Maintainer and This Week In React editor + - name: slorber + imageURL: https://github.com/slorber.png + title: Docusaurus Maintainer and This Week In React editor editor editor editor editor editor editor editor + - name: slorber + imageURL: https://github.com/slorber.png + title: Docusaurus Maintainer and This Week In React editor + - name: slorber + imageURL: https://github.com/slorber.png + socials: + twitter: sebastienlorber + github: slorber + - name: slorber + imageURL: https://github.com/slorber.png + socials: + twitter: sebastienlorber + github: slorber + - name: slorber + imageURL: https://github.com/slorber.png + socials: + a: sebastienlorber + b: slorber + c: slorber + d: slorber + e: slorber + f: slorber + g: slorber + h: slorber + i: slorber + j: slorber + k: slorber + l: slorber + - name: slorber + imageURL: https://github.com/slorber.png + socials: + a: sebastienlorber + b: slorber + c: slorber + d: slorber + e: slorber + f: slorber + g: slorber + h: slorber + i: slorber + j: slorber + k: slorber + l: slorber +--- + +# 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..03813a689f52 --- /dev/null +++ b/website/_dogfooding/_blog tests/2024-07-03-single-author.mdx @@ -0,0 +1,18 @@ +--- +title: Single author socials +authors: + - name: slorber + imageURL: https://github.com/slorber.png + socials: + twitter: sebastienlorber + github: slorber + stackoverflow: https://stackoverflow.com/users/82609/sebastien-lorber + linkedin: https://www.linkedin.com/in/sebastienlorber/ + newsletter: https://thisweekinreact.com/newsletter +--- + +# Multiple authors + +## Content + +Content about the blog post diff --git a/website/blog/authors.yml b/website/blog/authors.yml index 7fb45d8003ec..3d5f5c6d2529 100644 --- a/website/blog/authors.yml +++ b/website/blog/authors.yml @@ -19,8 +19,11 @@ slorber: 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: + github: slorber + twitter: sebastienlorber + stackoverflow: /82609/sebastien-lorber yangshun: name: Yangshun Tay