refactor(theme): social icon support all iconify icons, close #781 (#790)

This commit is contained in:
pengzhanbo 2025-12-12 20:40:50 +08:00 committed by GitHub
parent c42a601467
commit 95d345bf6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 328 additions and 138 deletions

View File

@ -392,31 +392,55 @@ export default defineUserConfig({
将作为 图标链接 展示在 导航栏最右侧。
图标可选值:
- `'github'`
- `'gitlab'`
- `'npm'`
- `'docker'`
- `'discord'`
- `'telegram'`
- `'facebook'`
- `'instagram'`
- `'linkedin'`
- `'mastodon'`
- `'slack'`
- `'twitter'`
- `'x'`
- `'youtube'`
- `'juejin'`
- `'stackoverflow'`
- `'qq'`
- `'weibo'`
- `'bilibili'`
- `'zhihu'`
- `'douban'`
- `'steam'`
- `'xbox'`
- `{ svg: string, name?: string }`: 自定义图标,传入 svg 源码字符串,可选 `name` 字段,用于配置 [`navbarSocialInclude`](#navbarsocialinclude)
支持 [Iconify](https://icon-sets.iconify.design/) 任意图标,直接使用 iconify name 即可自动加载。
对于 `simple-icons` 集合下的图标,可以省略 `simple-icons:` 前缀,如 `simple-icons:github` 可以简写为 `github`
常见的社交图标示例:
::: flex
<div style="flex: 1">
- discord ::simple-icons:discord::
- telegram ::simple-icons:telegram::
- facebook ::simple-icons:facebook::
- github ::simple-icons:github::
- instagram ::simple-icons:instagram::
- linkedin ::simple-icons:linkedin::
- mastodon ::simple-icons:mastodon::
- npm ::simple-icons:npm::
- slack ::simple-icons:slack::
- twitter ::simple-icons:twitter::
- x ::simple-icons:x::
- youtube ::simple-icons:youtube::
- bluesky ::simple-icons:bluesky::
- tiktok ::simple-icons:tiktok::
</div><div style="flex: 1">
- qq ::simple-icons:qq::
- weibo ::simple-icons:sinaweibo::
- bilibili ::simple-icons:bilibili::
- gitlab ::simple-icons:gitlab::
- docker ::simple-icons:docker::
- juejin ::simple-icons:juejin::
- zhihu ::simple-icons:zhihu::
- douban ::simple-icons:douban::
- steam ::simple-icons:steam::
- stackoverflow ::simple-icons:stackoverflow::
- xbox ::simple-icons:xbox::
- kuaishou ::simple-icons:kuaishou::
- twitch ::simple-icons:twitch::
- xiaohongshu ::simple-icons:xiaohongshu::
</div>
:::
[你可以在这里查看 **simple-icons** 所有可用图标](https://icon-sets.iconify.design/simple-icons/){.readmore}
如果 **Iconify** 无法满足你的需求,可以传入 `{ svg: string, name?: string }`的格式,使用自定义图标,传入 svg 源码字符串,可选 `name` 字段,用于配置 [`navbarSocialInclude`](#navbarsocialinclude)
示例:
@ -424,8 +448,10 @@ export default defineUserConfig({
export default defineUserConfig({
theme: plumeTheme({
social: [
// 使用 iconify name
{ icon: 'github', link: 'https://github.com/zhangsan' },
{
// 使用自定义图标
icon: { svg: '<svg>xxxxx</svg>', name: 'xxx' },
link: 'https://xxx.com'
},

View File

@ -397,31 +397,57 @@ export default defineUserConfig({
Displayed as icon links on the far right of the navbar.
Available icon options:
- `'github'`
- `'gitlab'`
- `'npm'`
- `'docker'`
- `'discord'`
- `'telegram'`
- `'facebook'`
- `'instagram'`
- `'linkedin'`
- `'mastodon'`
- `'slack'`
- `'twitter'`
- `'x'`
- `'youtube'`
- `'juejin'`
- `'stackoverflow'`
- `'qq'`
- `'weibo'`
- `'bilibili'`
- `'zhihu'`
- `'douban'`
- `'steam'`
- `'xbox'`
- `{ svg: string, name?: string }`: Custom icon, pass the SVG source string. The optional `name` field is used to configure [`navbarSocialInclude`](#navbarsocialinclude).
Supports any icon from [Iconify](https://icon-sets.iconify.design/). Simply use the iconify name to load it automatically.
For icons in the `simple-icons` collection, you can omit the `simple-icons:` prefix.
For example, `simple-icons:github` can be abbreviated as `github`.
Examples of common social icons:
::: flex
<div style="flex: 1">
- discord ::simple-icons:discord::
- telegram ::simple-icons:telegram::
- facebook ::simple-icons:facebook::
- github ::simple-icons:github::
- instagram ::simple-icons:instagram::
- linkedin ::simple-icons:linkedin::
- mastodon ::simple-icons:mastodon::
- npm ::simple-icons:npm::
- slack ::simple-icons:slack::
- twitter ::simple-icons:twitter::
- x ::simple-icons:x::
- youtube ::simple-icons:youtube::
- bluesky ::simple-icons:bluesky::
- tiktok ::simple-icons:tiktok::
</div><div style="flex: 1">
- qq ::simple-icons:qq::
- weibo ::simple-icons:sinaweibo::
- bilibili ::simple-icons:bilibili::
- gitlab ::simple-icons:gitlab::
- docker ::simple-icons:docker::
- juejin ::simple-icons:juejin::
- zhihu ::simple-icons:zhihu::
- douban ::simple-icons:douban::
- steam ::simple-icons:steam::
- stackoverflow ::simple-icons:stackoverflow::
- xbox ::simple-icons:xbox::
- kuaishou ::simple-icons:kuaishou::
- twitch ::simple-icons:twitch::
- xiaohongshu ::simple-icons:xiaohongshu::
</div>
:::
[You can view all available icons for **simple-icons** here](https://icon-sets.iconify.design/simple-icons/){.readmore}
If **Iconify** does not meet your needs, you can pass in the format `{ svg: string, name?: string }` to use a custom icon.
Pass in the SVG source code string, with the optional `name` field for configuring [`navbarSocialInclude`](#navbarsocialinclude).
Example:
@ -429,8 +455,10 @@ Example:
export default defineUserConfig({
theme: plumeTheme({
social: [
// use iconify name
{ icon: 'github', link: 'https://github.com/zhangsan' },
{
// use custom icon
icon: { svg: '<svg>xxxxx</svg>', name: 'xxx' },
link: 'https://xxx.com'
},

View File

@ -450,9 +450,15 @@ export default defineUserConfig({
type: 'post',
dir: 'blog',
title: 'Blog',
// [!code hl:4]
// [!code hl:9]
social: [
{ icon: 'github', link: 'https://github.com/pengzhanbo' },
// use iconify name
{ icon: 'github', link: 'https://github.com/zhangsan' },
{
// use custom icon
icon: { svg: '<svg>xxxxx</svg>', name: 'xxx' },
link: 'https://xxx.com'
},
],
}
]
@ -471,9 +477,15 @@ export default defineThemeConfig({
type: 'post',
dir: 'blog',
title: 'Blog',
// [!code hl:4]
// [!code hl:9]
social: [
{ icon: 'github', link: 'https://github.com/pengzhanbo' },
// use iconify name
{ icon: 'github', link: 'https://github.com/zhangsan' },
{
// use custom icon
icon: { svg: '<svg>xxxxx</svg>', name: 'xxx' },
link: 'https://xxx.com'
},
],
}
]
@ -482,43 +494,58 @@ export default defineThemeConfig({
:::
### Built-in Icon Library
Supports any icon from [Iconify](https://icon-sets.iconify.design/). Simply use the iconify name to load it automatically.
For icons in the `simple-icons` collection, the `simple-icons:` prefix can be omitted.
For example, `simple-icons:github` can be abbreviated as `github`.
Examples of common social icons:
::: flex
<div style="flex: 1">
- discord
- telegram
- facebook
- github
- instagram
- linkedin
- mastodon
- npm
- slack
- twitter
- x
- youtube
- discord ::simple-icons:discord::
- telegram ::simple-icons:telegram::
- facebook ::simple-icons:facebook::
- github ::simple-icons:github::
- instagram ::simple-icons:instagram::
- linkedin ::simple-icons:linkedin::
- mastodon ::simple-icons:mastodon::
- npm ::simple-icons:npm::
- slack ::simple-icons:slack::
- twitter ::simple-icons:twitter::
- x ::simple-icons:x::
- youtube ::simple-icons:youtube::
- bluesky ::simple-icons:bluesky::
- tiktok ::simple-icons:tiktok::
</div><div style="flex: 1">
- qq
- weibo
- bilibili
- gitlab
- docker
- juejin
- zhihu
- douban
- steam
- stackoverflow
- xbox
- qq ::simple-icons:qq::
- weibo ::simple-icons:sinaweibo::
- bilibili ::simple-icons:bilibili::
- gitlab ::simple-icons:gitlab::
- docker ::simple-icons:docker::
- juejin ::simple-icons:juejin::
- zhihu ::simple-icons:zhihu::
- douban ::simple-icons:douban::
- steam ::simple-icons:steam::
- stackoverflow ::simple-icons:stackoverflow::
- xbox ::simple-icons:xbox::
- kuaishou ::simple-icons:kuaishou::
- twitch ::simple-icons:twitch::
- xiaohongshu ::simple-icons:xiaohongshu::
</div>
:::
[You can view all available icons of **simple-icons** here](https://icon-sets.iconify.design/simple-icons/){.readmore}
If **Iconify** cannot meet your needs, you can pass in the format `{ svg: string, name?: string }`
to use custom icons by providing the SVG source code string.
## Article Cover Configuration
The article list page supports cover image display with various layout and size options.

View File

@ -429,7 +429,7 @@ export default defineThemeConfig({
## 社交链接
个人信息区域支持社交链接配置,未配置时继承[主题默认 social 设置](../../config/theme.md#social)。
个人信息区域支持社交链接配置,未配置时继承 [主题默认 social 设置](../../config/theme.md#social)。
::: code-tabs#config
@ -446,9 +446,15 @@ export default defineUserConfig({
type: 'post',
dir: 'blog',
title: '博客',
// [!code hl:4]
// [!code hl:9]
social: [
{ icon: 'github', link: 'https://github.com/pengzhanbo' },
// 使用 iconify name
{ icon: 'github', link: 'https://github.com/zhangsan' },
{
// 使用自定义图标
icon: { svg: '<svg>xxxxx</svg>', name: 'xxx' },
link: 'https://xxx.com'
},
],
}
]
@ -467,9 +473,15 @@ export default defineThemeConfig({
type: 'post',
dir: 'blog',
title: '博客',
// [!code hl:4]
// [!code hl:9]
social: [
{ icon: 'github', link: 'https://github.com/pengzhanbo' },
// 使用 iconify name
{ icon: 'github', link: 'https://github.com/zhangsan' },
{
// 使用自定义图标
icon: { svg: '<svg>xxxxx</svg>', name: 'xxx' },
link: 'https://xxx.com'
},
],
}
]
@ -478,43 +490,56 @@ export default defineThemeConfig({
:::
### 内置图标库
支持 [Iconify](https://icon-sets.iconify.design/) 任意图标,直接使用 iconify name 即可自动加载。
对于 `simple-icons` 集合下的图标,可以省略 `simple-icons:` 前缀,如 `simple-icons:github` 可以简写为 `github`
常见的社交图标示例:
::: flex
<div style="flex: 1">
- discord
- telegram
- facebook
- github
- instagram
- linkedin
- mastodon
- npm
- slack
- twitter
- x
- youtube
- discord ::simple-icons:discord::
- telegram ::simple-icons:telegram::
- facebook ::simple-icons:facebook::
- github ::simple-icons:github::
- instagram ::simple-icons:instagram::
- linkedin ::simple-icons:linkedin::
- mastodon ::simple-icons:mastodon::
- npm ::simple-icons:npm::
- slack ::simple-icons:slack::
- twitter ::simple-icons:twitter::
- x ::simple-icons:x::
- youtube ::simple-icons:youtube::
- bluesky ::simple-icons:bluesky::
- tiktok ::simple-icons:tiktok::
</div><div style="flex: 1">
- qq
- weibo
- bilibili
- gitlab
- docker
- juejin
- zhihu
- douban
- steam
- stackoverflow
- xbox
- qq ::simple-icons:qq::
- weibo ::simple-icons:sinaweibo::
- bilibili ::simple-icons:bilibili::
- gitlab ::simple-icons:gitlab::
- docker ::simple-icons:docker::
- juejin ::simple-icons:juejin::
- zhihu ::simple-icons:zhihu::
- douban ::simple-icons:douban::
- steam ::simple-icons:steam::
- stackoverflow ::simple-icons:stackoverflow::
- xbox ::simple-icons:xbox::
- kuaishou ::simple-icons:kuaishou::
- twitch ::simple-icons:twitch::
- xiaohongshu ::simple-icons:xiaohongshu::
</div>
:::
[你可以在这里查看 **simple-icons** 所有可用图标](https://icon-sets.iconify.design/simple-icons/){.readmore}
如果 **Iconify** 无法满足你的需求,可以传入 `{ svg: string, name?: string }`的格式,使用自定义图标,传入 svg 源码字符串。
## 文章封面配置
文章列表页支持封面图展示,提供多种布局和尺寸选项。

View File

@ -3,7 +3,7 @@ import type { IconifyIcon } from '@iconify/vue/offline'
import { loadIcon } from '@iconify/vue'
import { Icon as OfflineIcon } from '@iconify/vue/offline'
import { computed, ref, watch } from 'vue'
import { useIconsData } from '../composables/index.js'
import { normalizeIconClassname } from '../composables/index.js'
defineOptions({
inheritAttrs: false,
@ -20,15 +20,13 @@ const { name, size, color, prefix, extra } = defineProps<{
const icon = ref<IconifyIcon | null>(null)
const loaded = ref(false)
const iconsData = useIconsData()
const iconName = computed(() => {
if (name.includes(':'))
return name
return prefix ? `${prefix}:${name}` : name
})
const localIconName = computed(() => iconsData.value[iconName.value])
const localIconName = computed(() => normalizeIconClassname(iconName.value))
async function loadRemoteIcon() {
if (icon.value)

View File

@ -1,6 +1,8 @@
<script lang="ts" setup>
import type { SocialLinkIcon } from '../../shared/index.js'
import VPIcon from '@theme/VPIcon.vue'
import { computed } from 'vue'
import { socialFallbacks } from '../composables/index.js'
const { icon, link, ariaLabel } = defineProps<{
icon: SocialLinkIcon
@ -8,10 +10,22 @@ const { icon, link, ariaLabel } = defineProps<{
ariaLabel?: string
}>()
const svg = computed(() => {
if (typeof icon === 'object')
return icon.svg
return `<span class="vpi-social-${icon}" />`
const iconName = computed(() => {
if (typeof icon === 'string') {
const name = socialFallbacks[icon] || icon
if (name.includes(':'))
return name
return `simple-icons:${name}`
}
return icon
})
const label = computed(() => {
if (ariaLabel)
return ariaLabel
if (typeof icon === 'string')
return icon.includes(':') ? icon.split(':')[1] : icon
return icon.name
})
</script>
@ -19,9 +33,12 @@ const svg = computed(() => {
<a
class="vp-social-link no-icon"
:href="link"
:aria-label="ariaLabel ?? (typeof icon === 'string' ? icon : '')"
target="_blank" rel="noopener" v-html="svg"
/>
:aria-label="label"
:title="label"
target="_blank" rel="noopener"
>
<VPIcon :name="iconName" />
</a>
</template>
<style scoped>
@ -40,7 +57,7 @@ const svg = computed(() => {
}
.vp-social-link > :deep(svg),
.vp-social-link > :deep([class^="vpi-social-"]) {
.vp-social-link > :deep([class*="vpi-"]) {
width: 20px;
height: 20px;
fill: currentcolor;

View File

@ -2,15 +2,26 @@ import type { Ref } from 'vue'
import { icons } from '@internal/iconify'
import { ref } from 'vue'
type IconsData = Record<string, string>
type IconsDataRef = Ref<IconsData>
type NeedBackgroundIcons = string[]
type IconsDataRef = Ref<NeedBackgroundIcons>
const iconsData: IconsDataRef = ref(icons)
export const useIconsData = (): IconsDataRef => iconsData
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateIcons = (data: IconsData) => {
__VUE_HMR_RUNTIME__.updateIcons = (data: NeedBackgroundIcons) => {
iconsData.value = data
}
}
// 旧版本内置图标别名,映射回 simple-icons 集合中的名称
export const socialFallbacks: Record<string, string> = {
twitter: 'x',
weibo: 'sinaweibo',
}
export function normalizeIconClassname(icon: string): string {
const [collect, name] = icon.split(':')
return `vpi-${collect}-${name}${iconsData.value.includes(icon) ? ' bg' : ''}`
}

View File

@ -74,7 +74,7 @@ declare module '@internal/encrypt' {
}
declare module '@internal/iconify' {
const icons: Record<string, string>
const icons: string[]
export {
icons,
}

View File

@ -3,7 +3,8 @@
@import url("./vars.css");
@import url("./normalize.css");
@import url("./icons.css");
@import url("./social-icons.css");
/* @import url("./social-icons.css"); */
@import url("./compat.css");
@import url("./utils.css");
@import url("./content.css");

View File

@ -1,6 +1,6 @@
import type { App, Page } from 'vuepress'
import type { IconOptions } from 'vuepress-plugin-md-power'
import type { ThemeHomeConfig, ThemeNavItem, ThemeOptions, ThemeSidebar } from '../../shared/index.js'
import type { FriendGroup, FriendsItem, SocialLink, ThemeHomeConfig, ThemeNavItem, ThemeOptions, ThemeSidebar } from '../../shared/index.js'
import type { FsCache } from '../utils/index.js'
import { getIconContentCSS, getIconData } from '@iconify/utils'
import { isArray, uniq } from '@pengzhanbo/utils'
@ -8,7 +8,7 @@ import { entries, isLinkAbsolute, isLinkHttp, isPlainObject } from '@vuepress/he
import { isPackageExists } from 'local-pkg'
import { fs } from 'vuepress/utils'
import { getThemeConfig } from '../loadConfig/index.js'
import { createFsCache, interopDefault, logger, nanoid, perf, resolveContent, writeTemp } from '../utils/index.js'
import { createFsCache, interopDefault, logger, perf, resolveContent, writeTemp } from '../utils/index.js'
interface IconData {
className: string
@ -34,11 +34,17 @@ let fsCache: FsCache<IconDataMap> | null = null
// { iconName: { className, content } }
const cache: IconDataMap = {}
// 旧版本内置图标别名,映射回 simple-icons 集合中的名称
const socialFallbacks: Record<string, string> = {
twitter: 'x',
weibo: 'sinaweibo',
}
export async function prepareIcons(app: App): Promise<void> {
perf.mark('prepare:icons:total')
const options = getThemeConfig()
if (!isInstalled) {
await writeTemp(app, JS_FILENAME, resolveContent(app, { name: 'icons', content: '{}' }))
await writeTemp(app, JS_FILENAME, resolveContent(app, { name: 'icons', content: '[]' }))
return
}
if (!fsCache && app.env.isDev) {
@ -86,9 +92,10 @@ export async function prepareIcons(app: App): Promise<void> {
perf.log('prepare:icons:imports')
let cssCode = ''
const map: Record<string, string> = {}
const shouldBackground: string[] = []
for (const [iconName, { className, content, background }] of entries(cache)) {
map[iconName] = `${className}${background ? ' bg' : ''}`
if (background)
shouldBackground.push(iconName)
cssCode += `.${className} {\n --icon: ${content};\n}\n`
}
@ -96,7 +103,7 @@ export async function prepareIcons(app: App): Promise<void> {
writeTemp(app, CSS_FILENAME, cssCode),
writeTemp(app, JS_FILENAME, resolveContent(app, {
name: 'icons',
content: map,
content: shouldBackground,
before: `import './iconify.css'`,
})),
])
@ -106,10 +113,11 @@ export async function prepareIcons(app: App): Promise<void> {
perf.log('prepare:icons:total')
}
function isIconify(icon: any): icon is string {
function isIconify(icon: unknown): icon is string {
if (!icon || typeof icon !== 'string' || isLinkAbsolute(icon) || isLinkHttp(icon))
return false
return icon[0] !== '{' && ICONIFY_NAME.test(icon)
const ic = icon.trim()
return ic[0] !== '{' && ICONIFY_NAME.test(ic)
}
function withPrefix(icon: string, prefix?: string): string {
@ -130,7 +138,7 @@ function getIconsWithPage(page: Page, { provider = 'iconify', prefix }: IconOpti
}
const addIcon = (icon: unknown): void => {
if (isIconify(icon) && (provider === 'iconify' || icon.startsWith('iconify'))) {
if (icon && isIconify(icon) && (provider === 'iconify' || icon.startsWith('iconify'))) {
list.push(withPrefix(icon.replace(/^iconify /, ''), prefix))
}
}
@ -153,30 +161,58 @@ function getIconsWithPage(page: Page, { provider = 'iconify', prefix }: IconOpti
}
}
}
if (fm.pageLayout === 'friends') {
const socialList: SocialLink[] = []
if ((fm.list as FriendsItem[])?.length) {
for (const { socials } of fm.list as FriendsItem[]) {
socialList.push(...(socials || []))
}
}
if ((fm.groups as FriendGroup[])?.length) {
for (const { list } of fm.groups as FriendGroup[]) {
if (!list?.length)
continue
for (const { socials } of list as FriendsItem[]) {
socialList.push(...(socials || []))
}
}
}
socialList.forEach(social => addIcon(getIconWithSocial(social)))
}
return list
}
function getIconWithThemeConfig(options: ThemeOptions, { provider = 'iconify', prefix }: IconOptions): string[] {
const list: string[] = []
// navbar notes sidebar
// navbar / doc collection sidebar / social
const locales = options.locales || {}
entries(locales).forEach(([, { navbar, sidebar, collections }]) => {
entries(locales).forEach(([, { navbar, sidebar, collections, social }]) => {
// navbar icon
if (navbar) {
list.push(...getIconWithNavbar(navbar))
}
// social
const socialList: SocialLink[] = social ? [...social] : []
// sidebar icon
const sidebarList: ThemeSidebar[] = Object.values(sidebar || {}) as ThemeSidebar[]
if (collections?.length) {
collections.forEach((collection) => {
if (collection.type === 'doc' && collection.sidebar)
sidebarList.push(collection.sidebar)
if (collection.type === 'post' && collection.social)
socialList.push(...collection.social)
})
}
sidebarList.forEach(sidebar => list.push(...getIconWithSidebar(sidebar)))
// social
socialList.forEach(social => list.push(getIconWithSocial(social)))
})
const addIcon = (icon: unknown): string | void => {
if (isIconify(icon) && (provider === 'iconify' || icon.startsWith('iconify'))) {
if (icon && isIconify(icon) && (provider === 'iconify' || icon.startsWith('iconify'))) {
return withPrefix(icon.replace(/^iconify /, ''), prefix)
}
}
@ -224,6 +260,15 @@ function getIconWithSidebar(sidebar: ThemeSidebar): string[] {
return list
}
function getIconWithSocial({ icon }: SocialLink): string {
if (!icon || typeof icon !== 'string')
return ''
const name = socialFallbacks[icon] || icon
if (name.includes(':'))
return name
return `simple-icons:${name}`
}
async function resolveCollect(collect: string, names: string[]) {
const filepath = locate(collect)
const config = await readJSON(filepath)
@ -251,7 +296,7 @@ async function resolveCollect(collect: string, names: string[]) {
*/
const background = !data.body.includes('currentColor')
cache[icon] = {
className: `vpi-${nanoid()}`,
className: normalizeClassname(icon),
background,
content: matched,
}
@ -260,6 +305,11 @@ async function resolveCollect(collect: string, names: string[]) {
return unknownList
}
function normalizeClassname(icon: string): string {
const [collect, name] = icon.split(':')
return `vpi-${collect}-${name}`
}
async function readJSON(filepath: string) {
try {
return await fs.readJSON(filepath, 'utf-8')

View File

@ -1,3 +1,4 @@
import type { LiteralUnion } from '../utils.js'
/**
*
*/
@ -10,7 +11,7 @@ export interface SocialLink {
/**
*
*/
export type SocialLinkIcon = SocialLinkIconUnion | { svg: string, name?: string }
export type SocialLinkIcon = LiteralUnion<SocialLinkIconUnion> | { svg: string, name?: string }
export type SocialLinkIconUnion
= | 'discord'
@ -36,3 +37,9 @@ export type SocialLinkIconUnion
| 'steam'
| 'stackoverflow'
| 'xbox'
| 'tiktok'
| 'kuaishou'
| 'bytedance'
| 'xiaohongshu'
| 'bluesky'
| 'gmail'