Automatic Social Share Images

Eugene Musebe

Social media is amongst the main sources of website traffic this day. We are always engaging our users to share content as much as possible. Let us learn how we can ensure our websites look elegant when shared by dynamically generating relevant social sharing images.

Codesandbox

The final project can be viewed on Codesandbox.

You can find the full source code on my [Github](https://github.com/musebe/Social-sharing-images-nuxt.git) repository.

Creating our blog

Nuxt.Js initialization

Before learning how to generate social share images, we need to set up our blog. Let us create our NuxtJs project.

We will set up our project using the create-nuxt-app utility. Ensure you have yarn or npm v5.2+ or v6.1+ installed.

1yarn create nuxt-app automatic-social-share-nuxtjs
2
3# OR
4
5npx crete-nuxt-app automatic-social-share-nuxtjs
6
7# OR
8
9npm init nuxt-app automatic-social-share-nuxtjs

This will prompt a series of setup questions. Here are our recommended defaults:

Project name: automatic-social-share-nuxtjs

Programming language: JavaScript

Package manager: Yarn

UI framework: Tailwind CSS

Nuxt.js modules: Content

Linting tools: N/A

Testing frameworks: None

Rendering mode: Universal (SSR/SSG)

Deployment target: Server (Node.js hosting)

Development tools: N/A

What is your Github username? <your-github-username>

Version control system: Git

Once the setup is complete, you may now run the project. The project should now be available on http://localhost:3000

1yarn dev
2
3# OR
4
5npm run dev

Adding dummy content

We need some content to display on our dummy blog. We have some prepared for you. Let us copy this content into the content/articles folder.

1mkdir content/articles
2
3cd content/articles
4
5wget https://raw.githubusercontent.com/musebe/Social-sharing-images-nuxt/main/content/articles/curabitur.md
6
7wget https://raw.githubusercontent.com/musebe/Social-sharing-images-nuxt/main/content/articles/donec.md
8
9wget https://raw.githubusercontent.com/musebe/Social-sharing-images-nuxt/main/content/articles/duis.md
10
11wget https://raw.githubusercontent.com/musebe/Social-sharing-images-nuxt/main/content/articles/nam.md

In each of the files, you will notice that the image is a simple string instead of a URL to a remote image. This is because we are hosting our images on Cloudinary. Cloudinary is a powerful image management platform we will be using. If you do not have an account, feel free to create one here.

Create a folder called automatic-social-share-images-nuxtjs and add the following files:

Additionally, create a folder called template and add this file: og-template.png.

The resulting folder should look like this:

Cloudinary setup

To be able to render the images from Cloudinary in our project, we need to set up the recommended Nuxt.Js plugin.

1yarn add @nuxtjs/cloudinary
2
3# OR
4
5npm install @nuxtjs/cloudinary

Configure the plugin by adding a cloudinary section to the nuxt.config.js file. This is the project's configuration file.

1// nuxt.config.js
2
3export default {
4
5...
6
7cloudinary: {
8
9cloudName: process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,
10
11useComponent: true,
12
13secure: true
14
15}
16
17}

The above file references the NUXT_ENV_CLOUDINARY_CLOUD_NAME environmental variables. These are values we set outside of our codebase either for security or deployment flexibility. You can find your Cloudinary cloud name on your dashboard. Let us create the .env file.

1touch .env

You may now set the variable:

1NUXT_ENV_CLOUDINARY_CLOUD_NAME=<cloudinary-cloud-name>

The above setup will ensure that the $cloudinary instance is injected globally.

Article renderer

We are going to create two components, one to render the entire article and one to render the previews of other articles. Let us start with our single article renderer. Let us create a custom component to render the title, image, author details and content.

1touch components/ArticleRender.vue
1<!-- components/ArticleRender.vue -->
2
3<template>
4
5<div>
6
7...
8
9<div>
10
11<div>
12
13<h1>
14
15<span>{{ article.title }}</span>
16
17</h1>
18
19<img :src="$cloudinary.image.url(article.image)" />
20
21</div>
22
23<div>
24
25<div>
26
27<div>
28
29<div>
30
31<img
32
33:src="$cloudinary.image.url(article.author_avatar)"
34
35alt="article.author"
36
37/>
38
39</div>
40
41<div>
42
43<p>
44
45{{ article.author }}
46
47</p>
48
49</div>
50
51</div>
52
53</div>
54
55<nuxt-content :document="article" />
56
57</div>
58
59</div>
60
61</div>
62
63</template>
1// components/ArticleRender.vue
2
3<script>
4
5export default {
6
7props: {
8
9article: {
10
11required: true,
12
13type: Object,
14
15},
16
17},
18
19};
20
21</script>

The above component will receive the article as a required property and render it. The nuxt-content component renders our markdown content in a friendly HTML format.

Let us now setup the MoreArticles component. Create the component in the MoreComponents folder:

1touch components/MoreArticles.vue

This is the component we will use to display other articles available for reading.

1<!-- components/MoreArticles.vue -->
2
3<template>
4
5<div>
6
7...
8
9<div>
10
11<div>
12
13<h2>More articles</h2>
14
15</div>
16
17<div>
18
19<div v-for="(article, key) in articles" :key="key">
20
21<div>
22
23<img :src="$cloudinary.image.url(article.image)" />
24
25</div>
26
27<div>
28
29<div>
30
31<nuxt-link :to="article.path">
32
33<p>{{ article.title }}</p>
34
35<p>{{ article.description }}</p>
36
37</nuxt-link>
38
39</div>
40
41<div>
42
43<div>
44
45<nuxt-link :to="article.path">
46
47<span>{{ article.author }}</span>
48
49<img :src="$cloudinary.image.url(article.author_avatar)" />
50
51</nuxt-link>
52
53</div>
54
55<div>
56
57<p>
58
59<nuxt-link :to="article.path">
60
61{{ article.author }}
62
63</nuxt-link>
64
65</p>
66
67<div>
68
69<time :datetime="article.createdAt">
70
71{{ article.createdAt }}
72
73</time>
74
75</div>
76
77</div>
78
79</div>
80
81</div>
82
83</div>
84
85</div>
86
87</div>
88
89</div>
90
91</template>

Our component will an Array of articles.

1// components/MoreArticles.vue
2
3<script>
4
5export default {
6
7props: {
8
9articles: {
10
11required: true,
12
13type: Array,
14
15},
16
17},
18
19};
20
21</script>

Fetching all articles

The nuxt/content module globally injects the $context instance which means we can access it anywhere. We will use the asyncData hook to fetch the articles and merge them back into the Page state. Let's do this in the pages/index.vue page.

1// pages/index.vue
2
3<script>
4
5import ArticleRender from "../components/ArticleRender.vue";
6
7
8
9import MoreArticles from "../components/MoreArticles.vue";
10
11
12
13export default {
14
15components: {
16
17ArticleRender,
18
19MoreArticles,
20
21},
22
23
24
25async asyncData({ $content }) {
26
27const articles = await $content("articles").fetch();
28
29
30
31const article = articles.pop();
32
33
34
35return {
36
37article,
38
39articles,
40
41};
42
43},
44
45};
46
47</script>

In the above code, we render the last article and pass the rest to be displayed in the "More articles" section. As we created the rendering components already, let's add them to the page template section.

1<!-- pages/index.vue -->
2
3<template>
4
5<div>
6
7<article-render :article="article" />
8
9<more-articles :articles="articles" />
10
11</div>
12
13</template>

Fetching a single article

The pages/index.vue page is the index page, the page we receive when we open http://localhost:3000. Other articles will be rendered through the pages/articles/_slug.vue page. This page will receive the slug of the article we want to read. Let us first create the file.

1touch pages/articles/_slug.vue

We will use the slug to query the article we want to read and to exclude it in "More articles". To exclude the current article in the query, we use the $ne flag which denotes not equal.

1// pages/articles/_slug.vue
2
3<script>
4
5import ArticleRender from "../../components/ArticleRender.vue";
6
7
8
9import MoreArticles from "../../components/MoreArticles.vue";
10
11
12
13export default {
14
15components: {
16
17ArticleRender,
18
19MoreArticles,
20
21},
22
23async asyncData({ $content, params }) {
24
25const article = await $content("articles", params.slug).fetch();
26
27
28
29const articles = await $content("articles")
30
31.where({
32
33slug: { $ne: params.slug },
34
35})
36
37.fetch();
38
39
40
41return {
42
43article,
44
45articles,
46
47};
48
49},
50
51};

Let us now render the content as we did on the pages/index.vue page.

1<!-- pages/index.vue -->
2
3<template>
4
5<div>
6
7<article-render :article="article" />
8
9<more-articles :articles="articles" />
10
11</div>
12
13</template>

When we open our landing page, we should now see something similar to this:

Creating social share images

Social share images are the images displayed when the article is shared. Here is a preview of the social share image we will be generating.

To generate the image, we will be using a series of transformations applied on the template file. We will overlay the article's image, the article title, author image and author title.

1// pages/index.vue
2
3<script>
4
5...
6
7export default {
8
9...
10
11computed: {
12
13ogImageUrl() {
14
15return this.$cloudinary.image.url(
16
17"automatic-social-share-images-nuxtjs/template/og-template", {
18
19transformation: [{
20
21overlay: "automatic-social-share-images-nuxtjs:photo-1494253109108-2e30c049369b",
22
23gravity: "north",
24
25height: "400",
26
27width: "700",
28
29},
30
31{
32
33overlay: "automatic-social-share-images-nuxtjs:avataaars",
34
35gravity: "south_west",
36
37height: "50",
38
39width: "50",
40
41x: "250",
42
43y: "100",
44
45},
46
47{
48
49overlay: {
50
51font_family: "Arial",
52
53font_size: 24,
54
55font_weight: "bold",
56
57text: this.article.title,
58
59},
60
61gravity: "south_west",
62
63co_rgb: "000000",
64
65x: "250",
66
67y: "175",
68
69},
70
71{
72
73overlay: {
74
75font_family: "Arial",
76
77font_size: 18,
78
79font_weight: "normal",
80
81text: this.article.author,
82
83},
84
85gravity: "south_west",
86
87co_rgb: "000000",
88
89x: "325",
90
91y: "115",
92
93},
94
95],
96
97}
98
99);
100
101},
102
103},
104
105...
106
107};
108
109</script>

To customise the content displayed when sharing the articles, we will use the open graph protocol. This is the protocol that enables any web page to become a rich object in a social graph. We do this by adding basic metadata to our header elements. Examples of these tags are og:site_name, og:type, og:url, og:title etc. Twitter also has special tags it uses for its social cards. You can read more about them here.

To append meta tags to our page's head (e.g. meta tags), we add a head method in our page script and return the key meta with the correct values.

1// pages/index.vue
2
3<script>
4
5
6export default {
7
8...
9
10head() {
11
12return {
13
14meta: [{
15
16property: "og:site_name",
17
18content: "NuxtJs Blog"
19
20},
21
22
23
24{
25
26hid: "og:type",
27
28property: "og:type",
29
30content: "article"
31
32},
33
34
35
36{
37
38hid: "og:url",
39
40property: "og:url",
41
42content: process.env.NUXT_ENV_URL,
43
44},
45
46
47
48{
49
50hid: "og:title",
51
52property: "og:title",
53
54content: this.article.title,
55
56},
57
58
59
60{
61
62hid: "og:description",
63
64property: "og:description",
65
66content: this.article.description,
67
68},
69
70
71
72{
73
74hid: "og:image",
75
76property: "og:image",
77
78content: this.ogImageUrl,
79
80},
81
82
83
84{
85
86property: "og:image:width",
87
88content: "1200"
89
90},
91
92
93
94{
95
96property: "og:image:height",
97
98content: "630"
99
100},
101
102
103
104{
105
106name: "twitter:site",
107
108content: "NuxtJs Blog"
109
110},
111
112
113
114{
115
116name: "twitter:card",
117
118content: "summary_large_image"
119
120},
121
122
123
124{
125
126hid: "twitter:url",
127
128name: "twitter:url",
129
130content: process.env.NUXT_ENV_URL,
131
132},
133
134
135
136{
137
138hid: "twitter:title",
139
140name: "twitter:title",
141
142content: this.article.title,
143
144},
145
146
147
148{
149
150hid: "twitter:description",
151
152name: "twitter:description",
153
154content: this.article.description,
155
156},
157
158
159
160{
161
162hid: "twitter:image",
163
164name: "twitter:image",
165
166content: this.ogImageUrl,
167
168},
169
170],
171
172};
173
174},
175
176...
177
178};
179
180</script>

Using the above configuration, we have a solid setup for social media sharing of our landing page. To enable the same for our pages/articles/_slug.vue page, we just need to copy over the head and computed methods.

This is just an introduction to what we can achieve. Feel free to read more on Open Graph protocol to see what is possible.

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.