Skip to content

Commit

Permalink
feat: twitter card (#80)
Browse files Browse the repository at this point in the history
* feat: twitter card

* support quote tweet

* typo

* cleanup

* cache nodes, prevent re-fetch on file change

* docs

* add props
  • Loading branch information
farnabaz authored Feb 17, 2021
1 parent 7bcd794 commit 060361b
Show file tree
Hide file tree
Showing 7 changed files with 718 additions and 26 deletions.
115 changes: 115 additions & 0 deletions docs/components/global/Tweet.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<template>
<div class="tweet">
<div class="author flex mb-4">
<a :href="profileUrl" target="_blank" rel="noopener noreferrer">
<img :src="avatar" class="w-6 h-6 rounded-full" :class="{'h-12 w-12': layout === 'tweet'}" />
</a>
<div class="ml-2 flex-1">
<a :href="profileUrl" target="_blank" rel="noopener noreferrer">
<span class="font-bold text-black dark:text-white" :class="{'block': layout === 'tweet'}">
{{ name }}
</span>
<span class="text-sm text-gray-400">@{{ username }}</span>
</a>
<template v-if="layout !== 'tweet'">
·
<a :href="tweetUrl" target="_blank" rel="noopener noreferrer" class="text-sm hover:text-blue-600">
{{ $d(createdAt, "long") }}
</a>
</template>
</div>
<a v-if="layout === 'tweet'" :href="tweetUrl" target="_blank" rel="noopener noreferrer">
<IconTwitter class="text-blue-500" />
</a>
</div>
<div class="content">
<slot />
</div>
<div v-if="layout === 'tweet'" class="mt-2 flex">
<a :href="likeUrl" target="_blank" rel="noopener noreferrer" class="flex items-center hover:text-red-600">
<IconHeart class="mr-2" />
{{ heartCount }}
</a>
<a :href="tweetUrl" target="_blank" rel="noopener noreferrer" class="ml-4 hover:text-blue-600">
{{ $d(createdAt, "long") }}
</a>
</div>
</div>
</template>

<script>
export default {
props: {
id: { type: String, default: '' },
name: { type: String, default: '' },
username: { type: String, default: '' },
avatar: { type: String, default: '' },
heartCount: { type: String, default: '' },
createdAt: { type: Number, default: 0 },
layout: { type: String, default: 'tweet' }
},
computed: {
tweetUrl () {
return `https://twitter.com/${this.username}/status/${this.id}`
},
profileUrl () {
return `https://twitter.com/${this.username}`
},
likeUrl () {
return `https://twitter.com/intent/like?tweet_id=${this.id}`
}
}
}
</script>

<style>
.tweet {
@apply my-5 p-6 pb-3 border border-gray-300 rounded-md mx-auto;
@apply dark:border-gray-700;
width: calc(min(100%, 550px))!important;
}
.tweet.tweet-quote {
@apply p-3;
}
.tweet .emoji {
@apply w-5 h-5 m-0 inline-block;
}
.tweet .content p > a {
@apply text-blue-600;
}
.tweet .image-container-1 {
@apply mt-2 flex flex-wrap rounded-md overflow-hidden;
}
.tweet .image-container-1 .media-image {
@apply w-full object-cover;
}
.tweet .image-container-2 {
@apply mt-2 grid grid-cols-2 gap-1 rounded-md overflow-hidden;
}
.tweet .image-container-2 .media-image {
height: 300px;
@apply object-cover;
}
.tweet .image-container-3 {
@apply mt-2 grid grid-cols-2 gap-1 grid-rows-2 rounded-md overflow-hidden;
}
.tweet .image-container-3 .media-image {
height: 150px;
@apply object-cover;
}
.tweet .image-container-3 .media-image:nth-child(3n+2) {
height: 100%;
@apply row-span-2;
}
.tweet .image-container-4 {
@apply mt-2 grid grid-cols-2 gap-1 grid-rows-2 rounded-md overflow-hidden;
}
.tweet .image-container-4 .media-image {
height: 150px;
@apply object-cover;
}
</style>
27 changes: 27 additions & 0 deletions docs/content/2.usage/03.components.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,30 @@ link: https://codesandbox.io/embed/nuxt-content-l164h?hidenavigation=1&theme=dar
| Prop | Type | Required | Value |
|---------|------|-------------| ---|
| `src` | `String` | `true` | Url to CodeSandbox embed |

### `<tweet>`

Embed Tweets easily in your documentation with great performances, tweets embed statically without using any runtime JS to render.

<code-group>
<code-block label="Preview" active>
<div class="p-4 pb-0 border-2 border-t-0 border-gray-700 rounded-b-md">

<tweet id="1314628331841761289" />

</div>


</code-block>
<code-block label="Code">

```md
<tweet id="1314628331841761289" />
```

</code-block>
</code-group>

| Prop | Type | Required | Value |
|---------|------|-------------| ---|
| `id` | `String` | `true` | Tweet id |
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"ohmyfetch": "^0.1.6",
"postcss": "^7",
"prism-themes": "^1.5.0",
"static-tweets": "^0.2.7",
"tailwind-css-variables": "^2.0.3",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"theme-colors": "^0.0.5",
Expand Down
5 changes: 5 additions & 0 deletions theme/components/icons/IconHeart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<path class="icon" fill="currentColor" d="M12 21.638h-.014C9.403 21.59 1.95 14.856 1.95 8.478c0-3.064 2.525-5.754 5.403-5.754 2.29 0 3.83 1.58 4.646 2.73.813-1.148 2.353-2.73 4.644-2.73 2.88 0 5.404 2.69 5.404 5.755 0 6.375-7.454 13.11-10.037 13.156H12zM7.354 4.225c-2.08 0-3.903 1.988-3.903 4.255 0 5.74 7.035 11.596 8.55 11.658 1.52-.062 8.55-5.917 8.55-11.658 0-2.267-1.822-4.255-3.902-4.255-2.528 0-3.94 2.936-3.952 2.965-.23.562-1.156.562-1.387 0-.015-.03-1.426-2.965-3.955-2.965z" />
</svg>
</template>
3 changes: 2 additions & 1 deletion theme/nuxt.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export default docusOptions => ({
remarkPlugins: [
[r('utils/remark-prose'), {
proseClass: 'prose dark:prose-dark'
}]
}],
[r('utils/remark-tweet'), {}]
],
remarkAutolinkHeadings: {
behavior: 'wrap'
Expand Down
89 changes: 89 additions & 0 deletions theme/utils/remark-tweet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const { fetchTweetAst } = require('static-tweets')

function createTweetNode (data, content, layout = 'tweet') {
return [
{
type: 'html',
value: `<Tweet class="tweet tweet-${layout}" layout="${layout}" id="${data.id}" avatar="${data.avatar.normal}" :created-at="${data.createdAt}" heart-count="${data.heartCount}" name="${data.name}" username="${data.username}">`
},
...mapAST(content),
{ type: 'html', value: '</Tweet>' }
]
}

function mapAST (ast) {
return ast.flatMap((node) => {
if (typeof node === 'string') {
return { type: 'text', value: node }
}
if (node.tag === 'a') {
return { type: 'link', data: node.props, url: node.props.href, children: mapAST(node.nodes) }
}
if (node.tag === 'br') {
return { type: 'html', value: '<br />' }
}
if (node.tag === 'img') {
if (node.props.dataType === 'emoji-for-text') {
return { type: 'html', value: `<img src="${node.props.src}" alt="${node.props.alt}" class="emoji" width="28" height="28" />` }
}
if (node.props.dataType === 'media-image') {
const { props } = node
return {
type: 'html',
value: `<img src="${props.src}" alt="${props.alt}" class="media-image" width="${props.width}" height="${props.height}" />`
}
}
}
if (node.tag === 'div') {
return [
{ type: 'html', value: `<div class="${node.props.dataType.replace(/ /, '-')}">` },
...mapAST(node.nodes),
{ type: 'html', value: '</div>' }
]
}

if (node.tag === 'p') {
return { type: 'paragraph', children: mapAST(node.nodes) }
}

// qoute
if (node.tag === 'blockquote') {
const { data, nodes } = node.data.ast[0]
return createTweetNode(data, nodes, 'tweet tweet-quote')
}

return { type: 'text', value: '?' }
})
}

const tweetCache = {}

module.exports = () => {
return async (tree) => {
const modified = tree.children.map(async (node, i) => {
if (node.type === 'html' && node.value && node.value.match(/^\s*<[t|T]weet\s+/)) {
const match = node.value.match(/id=['"](\d*)['"]/)
if (!match) {
// eslint-disable-next-line no-console
console.error('Invalid tweet id')
return { type: 'html', value: '<!-- Invalid tweet id -->' }
}
if (!tweetCache[match[1]]) {
try {
const tweet = await fetchTweetAst(match[1])
const { nodes, data } = tweet[0]
tweetCache[match[1]] = createTweetNode(data, nodes)
} catch (e) {
// eslint-disable-next-line no-console
console.error(`Cannot fetch tweet ${match[1]}. ${e.message}`)
return { type: 'html', value: `<!-- Cannot fetch tweet ${match[1]} -->` }
}
}
return tweetCache[match[1]]
}
return Promise.resolve(node)
})
tree.children = (await Promise.all(modified)).flat()
return null
}
}
Loading

1 comment on commit 060361b

@vercel
Copy link

@vercel vercel bot commented on 060361b Feb 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.