Social share (#88)

add social sharing and og:meta
This commit is contained in:
Ilya Y 2023-01-30 13:39:36 +03:00 committed by ilya-bkv
parent 12f5479957
commit 714c11e591
14 changed files with 2723 additions and 2600 deletions

View File

@ -35,6 +35,8 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.216.0",
"@aws-sdk/lib-storage": "^3.223.0",
"@solid-primitives/share": "^2.0.1",
"astro-seo-meta": "^2.0.0",
"formidable": "^2.1.1",
"mailgun.js": "^8.0.2"
},

View File

@ -15,6 +15,7 @@ import { useSession } from '../../context/session'
import { ReactionKind } from '../../graphql/types.gen'
import CommentEditor from '../_shared/CommentEditor'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { getDescription } from '../../utils/meta'
type Props = {
comment: Reaction
@ -152,15 +153,17 @@ const Comment = (props: Props) => {
</button>
</Show>
<SharePopup
containerCssClass={stylesHeader.control}
trigger={
<button class={clsx(styles.commentControl, styles.commentControlShare)}>
<Icon name="share" class={styles.icon} />
{t('Share')}
</button>
}
/>
{/*<SharePopup*/}
{/* title={'artile.title'}*/}
{/* description={getDescription(body())}*/}
{/* containerCssClass={stylesHeader.control}*/}
{/* trigger={*/}
{/* <button class={clsx(styles.commentControl, styles.commentControlShare)}>*/}
{/* <Icon name="share" class={styles.icon} />*/}
{/* {t('Share')}*/}
{/* </button>*/}
{/* }*/}
{/*/>*/}
{/*<button*/}
{/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/}

View File

@ -7,6 +7,7 @@ import type { Author, Shout } from '../../graphql/types.gen'
import { t } from '../../utils/intl'
import MD from './MD'
import { SharePopup } from './SharePopup'
import { getDescription } from '../../utils/meta'
import stylesHeader from '../Nav/Header.module.scss'
import styles from '../../styles/Article.module.scss'
import { RatingControl } from './RatingControl'
@ -170,6 +171,9 @@ export const FullArticle = (props: ArticleProps) => {
<div class={styles.shoutStatsItem}>
<SharePopup
title={props.article.title}
description={getDescription(props.article.body)}
imageUrl={props.article.cover}
containerCssClass={stylesHeader.control}
trigger={<Icon name="share-outline" class={styles.icon} />}
/>

View File

@ -1,45 +1,59 @@
import { Icon } from '../_shared/Icon'
import { t } from '../../utils/intl'
import { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share'
import styles from '../_shared/Popup/Popup.module.scss'
import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
type SharePopupProps = Omit<PopupProps, 'children'>
type SharePopupProps = {
title: string
shareUrl?: string
imageUrl: string
description: string
} & Omit<PopupProps, 'children'>
export const SharePopup = (props: SharePopupProps) => {
const [share] = createSocialShare(() => ({
title: props.title,
url: props.shareUrl,
description: props.description
}))
const copyLink = async () => {
await navigator.clipboard.writeText(window.location.href)
}
return (
<Popup {...props} variant="bordered">
<ul class="nodash">
<li>
<a href="#">
<button role="button" onClick={() => share(VK)}>
<Icon name="vk-white" class={styles.icon} />
VK
</a>
</button>
</li>
<li>
<a href="#">
<button role="button" onClick={() => share(FACEBOOK)}>
<Icon name="facebook-white" class={styles.icon} />
Facebook
</a>
</button>
</li>
<li>
<a href="#">
<button role="button" onClick={() => share(TWITTER)}>
<Icon name="twitter-white" class={styles.icon} />
Twitter
</a>
</button>
</li>
<li>
<a href="#">
<button role="button" onClick={() => share(TELEGRAM)}>
<Icon name="telegram-white" class={styles.icon} />
Telegram
</a>
</button>
</li>
<li>
<a href="#">
<button role="button" onClick={copyLink}>
<Icon name="link-white" class={styles.icon} />
{t('Copy link')}
</a>
</button>
</li>
</ul>
</Popup>

View File

@ -1,5 +1,5 @@
import { t } from '../../utils/intl'
import { createMemo, For, Show } from 'solid-js'
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen'
import { capitalize } from '../../utils'
import { translit } from '../../utils/ru2en'
@ -11,6 +11,7 @@ import { CardTopic } from './CardTopic'
import { RatingControl } from '../Article/RatingControl'
import { SharePopup } from '../Article/SharePopup'
import stylesHeader from '../Nav/Header.module.scss'
import { getDescription } from '../../utils/meta'
interface ArticleCardProps {
settings?: {
@ -72,6 +73,12 @@ export const ArticleCard = (props: ArticleCardProps) => {
const { cover, layout, slug, authors, stat } = props.article
const [url, setUrl] = createSignal<string>(null)
onMount(() => {
const composeUrl = new URL(location.href)
setUrl(composeUrl.origin)
})
return (
<section
class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)}
@ -193,6 +200,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class={styles.shoutCardDetailsItem}>
<SharePopup
containerCssClass={stylesHeader.control}
title={props.article['title']}
description={getDescription(props.article['body'])}
imageUrl={props.article['cover']}
shareUrl={`${url()}/${slug}`}
trigger={
<button>
<Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} />

View File

@ -21,7 +21,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
const { topicEntities } = useTopicsStore()
const checkTopicIsSeen = (topicSlug: string) => {
return articlesByTopic()[topicSlug].every((article) => Boolean(seen()[article.slug]))
return articlesByTopic()[topicSlug]?.every((article) => Boolean(seen()[article.slug]))
}
const checkAuthorIsSeen = (authorSlug: string) => {
@ -97,7 +97,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }}
>
<small>@{authorSlug}</small>
{authorEntities()[authorSlug].name}
{authorEntities()[authorSlug]?.name}
</a>
</li>
)}

View File

@ -1,16 +1,16 @@
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import { createSignal, Show } from 'solid-js'
import MarkdownIt from 'markdown-it'
import { clsx } from 'clsx'
import styles from './Message.module.scss'
import DialogAvatar from './DialogAvatar'
import type { Message, ChatMember } from '../../graphql/types.gen'
import type { Message as MessageType, ChatMember } from '../../graphql/types.gen'
import formattedTime from '../../utils/formatDateTime'
import { Icon } from '../_shared/Icon'
import { MessageActionsPopup } from './MessageActionsPopup'
import QuotedMessage from './QuotedMessage'
type Props = {
content: Message
content: MessageType
ownId: number
members: ChatMember[]
replyClick?: () => void

View File

@ -10,6 +10,7 @@ import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { HeaderAuth } from './HeaderAuth'
import { SharePopup } from '../Article/SharePopup'
import { getDescription } from '../../utils/meta'
const resources: { name: string; route: keyof Routes }[] = [
{ name: t('zine'), route: 'home' },
@ -20,6 +21,8 @@ const resources: { name: string; route: keyof Routes }[] = [
type Props = {
title?: string
isHeaderFixed?: boolean
articleBody?: string
cover?: string
}
export const Header = (props: Props) => {
@ -123,6 +126,10 @@ export const Header = (props: Props) => {
<Show when={props.title}>
<div class={styles.articleControls}>
<SharePopup
title={props.title}
imageUrl={props.cover}
shareUrl={location.href}
description={getDescription(props.articleBody)}
onVisibilityChange={(isVisible) => {
setIsSharePopupVisible(isVisible)
}}

View File

@ -37,7 +37,7 @@ export const ArticlePage = (props: PageProps) => {
})
return (
<PageWrap headerTitle={article()?.title || ''}>
<PageWrap headerTitle={article()?.title || ''} articleBody={article()?.body} cover={article()?.cover}>
<Show when={Boolean(article())} fallback={<Loading />}>
<ArticleView article={article()} />
</Show>

View File

@ -8,6 +8,8 @@ import { clsx } from 'clsx'
type PageWrapProps = {
headerTitle?: string
articleBody?: string
cover?: string
children: JSX.Element
isHeaderFixed?: boolean
hideFooter?: boolean
@ -19,7 +21,12 @@ export const PageWrap = (props: PageWrapProps) => {
return (
<>
<Header title={props.headerTitle} isHeaderFixed={isHeaderFixed} />
<Header
title={props.headerTitle}
articleBody={props.articleBody}
cover={props.articleBody}
isHeaderFixed={isHeaderFixed}
/>
<main
class={clsx('main-content', props.class)}
classList={{ 'main-content--no-padding': !isHeaderFixed }}

View File

@ -2,20 +2,34 @@
import { setLocale } from './stores/ui'
import './styles/app.scss'
import { t } from './utils/intl'
import { Seo } from "astro-seo-meta"
// Setting locale for prerendered content here
const lang = Astro.url.searchParams.get('lang') || 'ru'
console.log('[main.astro] astro runtime locale is', lang)
setLocale(lang)
const { protocol, host} = Astro.url
const title = Astro.props.title ?? t('Discours');
const imageUrl = Astro.props.imageUrl ?? `${protocol}${host}/public/bonfire.png`
const description = Astro.props.description ?? t('Horizontal collaborative journalistic platform')
---
<html lang={lang || 'ru'}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/png" href="/favicon.png" />
<title>{t('Discours')}</title>
<Seo
title={title}
icon="favicon.png"
facebook={{
image: imageUrl,
type: "website",
}}
twitter={{
image: imageUrl,
card: description,
}}
/>
</head>
<body>
<slot />

View File

@ -3,21 +3,11 @@ import { Root } from '../components/Root'
import Prerendered from '../main.astro'
import { apiClient } from '../utils/apiClient'
import { initRouter } from '../stores/router'
import {getDescription} from '../utils/meta'
const excludes = [
'authors',
'connect',
'create',
'inbox',
'search',
'topics',
'welcome',
'confirm',
'feed'
]
const slug = Astro.params.slug?.toString()
if (slug.endsWith('.map') || slug in excludes) {
if (slug.includes('.')) {
return Astro.redirect('/404')
}
@ -30,8 +20,16 @@ const { pathname, search } = Astro.url
initRouter(pathname, search)
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
const title = article.title
const imageUrl = article.cover ?? ''
const description = article.body ? getDescription(article.body) : ''
---
<Prerendered>
<Prerendered
title={title}
imageUrl={imageUrl}
description={description}
>
<Root article={article} client:load />
</Prerendered>

8
src/utils/meta.ts Normal file
View File

@ -0,0 +1,8 @@
export const getDescription = (body: string) => {
if (!body) return null
const descriptionWordsArray = body
.slice(0, 150)
.replaceAll(/<[^>]*>/g, '')
.split(' ')
return descriptionWordsArray.splice(0, descriptionWordsArray.length - 1).join(' ') + '...'
}

5167
yarn.lock

File diff suppressed because it is too large Load Diff