Skip to content

Commit b5d782f

Browse files
committed
feat: updated open graph image to match site with real-time stargazer count
1 parent e186a3d commit b5d782f

29 files changed

+487
-81
lines changed

app/controllers/web/index.js

+56-45
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ const path = require('node:path');
88
const { Buffer } = require('node:buffer');
99

1010
const Boom = require('@hapi/boom');
11-
const QRCode = require('qrcode');
1211
const Meta = require('koa-meta');
12+
const QRCode = require('qrcode');
1313
const _ = require('lodash');
1414
const dayjs = require('dayjs-with-plugins');
1515
const humanize = require('humanize-string');
@@ -21,9 +21,12 @@ const revHash = require('rev-hash');
2121
const sanitizeHtml = require('sanitize-html');
2222
const sharp = require('sharp');
2323
const shortID = require('mongodb-short-id');
24+
const splitLines = require('split-lines');
2425
const titleize = require('titleize');
25-
const { isEmail } = require('validator');
26+
const wrap = require('word-wrap');
27+
const { Octokit } = require('@octokit/core');
2628
const { gzip } = require('node-gzip');
29+
const { isEmail } = require('validator');
2730

2831
const admin = require('./admin');
2932
const api = require('./api');
@@ -46,6 +49,7 @@ const Aliases = require('#models/aliases');
4649
const Domains = require('#models/domains');
4750
const Users = require('#models/users');
4851
const config = require('#config');
52+
const env = require('#config/env');
4953
// const createWebSocketAsPromised = require('#helpers/create-websocket-as-promised');
5054
const email = require('#helpers/email');
5155
const i18n = require('#helpers/i18n');
@@ -56,6 +60,30 @@ const { decrypt } = require('#helpers/encrypt-decrypt');
5660

5761
const meta = new Meta(config.meta, logger);
5862

63+
const octokit = new Octokit({
64+
auth: env.GITHUB_OCTOKIT_TOKEN
65+
});
66+
67+
// every 6 hours update github star count
68+
let STARS = 1000;
69+
async function checkGitHubStars() {
70+
const response = await octokit.request('GET /repos/{owner}/{repo}', {
71+
owner: 'forwardemail',
72+
repo: 'forwardemail.net',
73+
headers: {
74+
'X-GitHub-Api-Version': '2022-11-28'
75+
}
76+
});
77+
if (Number.isFinite(response?.data?.stargazers_count))
78+
STARS = response.data.stargazers_count;
79+
if (STARS <= 0) STARS = 1000;
80+
}
81+
82+
if (config.env !== 'test') {
83+
checkGitHubStars();
84+
setInterval(checkGitHubStars, ms('6h'));
85+
}
86+
5987
const SVG_STR = fs.readFileSync(
6088
config.env === 'development'
6189
? path.join(__dirname, '..', '..', '..', 'assets', 'img', 'template.svg')
@@ -205,14 +233,10 @@ async function generateOpenGraphImage(ctx, next) {
205233

206234
// load seo metadata
207235
let data = {};
208-
let match = `/${ctx.locale}`;
209236
let found = false;
210237
try {
211238
data = meta.getByPath(url, ctx.request.t);
212239
found = true;
213-
match = `/${ctx.locale}${url}`;
214-
if (match.length > 40)
215-
match = _.escape(_.unescape(match).slice(0, 40) + '...');
216240
} catch (err) {
217241
if (!keys.has(url)) logger.error(err);
218242
data = meta.getByPath('/', ctx.request.t);
@@ -223,8 +247,6 @@ async function generateOpenGraphImage(ctx, next) {
223247
for (const alternative of config.alternatives) {
224248
const slug = `/blog/best-${alternative.slug}-alternative`;
225249
if (url === slug) {
226-
match = `/${ctx.locale}${slug}`;
227-
228250
const title = ctx.state.t(
229251
'<span class="notranslate">%d</span> Best <span class="notranslate">%s</span> Alternatives in <span class="notranslate">%s</span>',
230252
config.alternatives.length - 1,
@@ -254,8 +276,6 @@ async function generateOpenGraphImage(ctx, next) {
254276
if (a.name === alternative.name) continue;
255277
const slug = `/blog/${alternative.slug}-vs-${a.slug}-email-service-comparison`;
256278
if (url === slug) {
257-
match = `/${ctx.locale}${slug}`;
258-
259279
const title = ctx.state.t(
260280
`<span class="notranslate">%s</span> vs <span class="notranslate">%s</span> Comparison (<span class="notranslate">%s</span>)`,
261281
alternative.name,
@@ -284,17 +304,13 @@ async function generateOpenGraphImage(ctx, next) {
284304
}
285305
}
286306

287-
if (match === '/') match = `/${ctx.locale}`;
288-
else if (match.endsWith('/')) match = match.slice(0, -1);
289-
if (match === `/${i18n.config.defaultLocale}`) match = '';
290-
291307
ctx.type = ctx.path.endsWith('.svg')
292308
? 'image/svg+xml'
293309
: ctx.path.endsWith('.jpeg')
294310
? 'image/jpeg'
295311
: 'image/png';
296312

297-
let [str] = data.title
313+
let [title] = data.title
298314
.replace(config.views.locals.striptags(config.metaTitleAffix), '')
299315
.replace(
300316
config.views.locals
@@ -303,17 +319,16 @@ async function generateOpenGraphImage(ctx, next) {
303319
''
304320
)
305321
.split(' - ');
306-
str = str.trim();
307-
if (url.startsWith('/guides') && str.includes(' for '))
308-
str = str.split(' for ')[1].trim();
309-
if (str.length > 50) str = _.escape(_.unescape(str).slice(0, 50) + '...');
310-
let freeEmail = ctx.translate('FREE_EMAIL');
311-
if (url.startsWith('/guides') || url.startsWith('/how-to'))
312-
freeEmail = ctx.translate('TUTORIAL');
322+
title = title.trim();
323+
if (url.startsWith('/guides') && title.includes(' for '))
324+
title = title.split(' for ')[1].trim();
325+
else if (title.includes(' for ')) title = title.split(' for ')[0].trim();
326+
if (title.length > 40)
327+
title = _.escape(_.unescape(title.trim()).slice(0, 40).trim() + '...');
313328

314329
// if it was a developer doc then parse the title
315330
const doc = config.views.locals.developerDocs.find((d) => d.slug === url);
316-
if (doc && isSANB(doc.ogBtnText)) freeEmail = doc.ogBtnText;
331+
if (doc && isSANB(doc.ogBtnText)) title = doc.ogBtnText.trim();
317332

318333
// if it was a open source guide then parse the title
319334
const platform = config.views.locals.platforms.find(
@@ -323,32 +338,28 @@ async function generateOpenGraphImage(ctx, next) {
323338
`/blog/open-source/${config.views.locals.dashify(p)}-email-clients` ===
324339
url
325340
);
326-
if (platform) freeEmail = platform;
341+
if (platform) title = platform.trim();
327342

328-
// fallback safeguard
329-
if (freeEmail.length > 20) freeEmail = i18n.translate('FREE_EMAIL', 'en');
343+
// remove year
344+
title = title.replace(`in ${dayjs().format('YYYY')}`, ' ').trim();
345+
title = title.replace(`for ${dayjs().format('YYYY')}`, ' ').trim();
346+
title = title.replace(dayjs().format('YYYY'), ' ').trim();
347+
title = title.replace('( )', '').trim();
330348

331-
let noCreditCard = ctx.translate('NO_CREDIT_CARD');
332-
if (noCreditCard.length > 60)
333-
noCreditCard = i18n.translate('NO_CREDIT_CARD', 'en');
349+
// fallback safeguard
350+
if (title.length > 24)
351+
title = i18n.translate('PRIVATE_BUSINESS', 'en').trim();
334352

335-
let urlBar = match;
336-
if (urlBar.length > 40) {
337-
urlBar = urlBar.slice(0, 40);
338-
urlBar += '...';
339-
}
353+
// LINE1, LINE2, LINE3
354+
const [line1, line2, line3] = splitLines(
355+
wrap(data.description.trim(), { width: 60 })
356+
);
340357

341-
const svgReplaced = SVG_STR.replace('NO_CREDIT_CARD', noCreditCard)
342-
.replaceAll('MONOSPACE_NAME', 'Inconsolata-dz for Powerline')
343-
.replaceAll('FONT_NAME', 'VC Honey')
344-
.replace('PRIVATE_BUSINESS', str.trim())
345-
.replace('FREE_EMAIL', freeEmail)
346-
.replace(
347-
'font-size="85"',
348-
`font-size="${freeEmail.length >= 14 ? 65 : 85}"`
349-
)
350-
.replace('forwardemail.net', `forwardemail.net${urlBar}`)
351-
.replace('font-size="56"', `font-size="${str.length >= 40 ? 40 : 56}"`);
358+
const svgReplaced = SVG_STR.replace('TITLE', title.trim())
359+
.replace('LINE1', line1 || '')
360+
.replace('LINE2', line2 || '')
361+
.replace('LINE3', line3 || '')
362+
.replace('COUNT', STARS);
352363

353364
const svg = Buffer.from(svgReplaced, 'utf8');
354365
const hash = revHash(ctx.type + ':' + svgReplaced);

assets/img/template.sketch

-45.9 KB
Binary file not shown.

assets/img/template.svg

+154-11
Loading

locales/ar.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -10323,5 +10323,15 @@
1032310323
"Calendar already exists.": "التقويم موجود بالفعل.",
1032410324
"Canonical": "قانوني",
1032510325
"Kubuntu": "كوبونتو",
10326-
"Lubuntu": "لوبونتو"
10326+
"Lubuntu": "لوبونتو",
10327+
"export format already supported).": "(تنسيق التصدير مدعوم بالفعل).",
10328+
"Webhook signature support": "دعم توقيع Webhook",
10329+
"header was added.": "تمت إضافة الرأس.",
10330+
"Bounce webhook support was added": "تمت إضافة دعم Bounce webhook",
10331+
"and we now allow users to send newsletters, announcements, and email marketing through our outbound SMTP service. We also added the ability to set domain-wide and alias-specific storage quotas for IMAP/POP3/CalDAV.": "والآن نسمح للمستخدمين بإرسال النشرات الإخبارية والإعلانات والتسويق عبر البريد الإلكتروني من خلال خدمة SMTP الصادرة. كما أضفنا القدرة على تعيين حصص تخزين على مستوى النطاق واسم مستعار محدد لـ IMAP/POP3/CalDAV.",
10332+
"Our service was founded in 2017 and powers email for over 500,000 domains – including notable users such as Canonical, Netflix, The Linux Foundation, several universities and governments, and more.": "تأسست خدمتنا في عام 2017 وهي تدعم البريد الإلكتروني لأكثر من 500000 نطاق - بما في ذلك مستخدمين بارزين مثل Canonical وNetflix وLinux Foundation والعديد من الجامعات والحكومات والمزيد.",
10333+
"Storage maximum quota for aliases on this domain name. Enter a value such as \"1 GB\" that will be parsed by": "الحد الأقصى لحصة التخزين للأسماء المستعارة على اسم المجال هذا. أدخل قيمة مثل \"1 جيجابايت\" التي سيتم تحليلها بواسطة",
10334+
"bytes": "بايتات",
10335+
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "الحد الأقصى لحصة التخزين لهذا الاسم المستعار. اتركه فارغًا لإعادة تعيين الحد الأقصى لحصة المجال الحالية أو أدخل قيمة مثل \"1 جيجابايت\" التي سيتم تحليلها بواسطة",
10336+
". This value can only be adjusted by domain admins.": "لا يمكن تعديل هذه القيمة إلا بواسطة مسؤولي المجال."
1032710337
}

locales/cs.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -10323,5 +10323,15 @@
1032310323
"Calendar already exists.": "Kalendář již existuje.",
1032410324
"Canonical": "Kanonický",
1032510325
"Kubuntu": "Kubuntu",
10326-
"Lubuntu": "Lubuntu"
10326+
"Lubuntu": "Lubuntu",
10327+
"export format already supported).": "exportní formát již podporován).",
10328+
"Webhook signature support": "Podpora podpisu webhooku",
10329+
"header was added.": "byla přidána hlavička.",
10330+
"Bounce webhook support was added": "Byla přidána podpora Bounce webhooku",
10331+
"and we now allow users to send newsletters, announcements, and email marketing through our outbound SMTP service. We also added the ability to set domain-wide and alias-specific storage quotas for IMAP/POP3/CalDAV.": "a nyní umožňujeme uživatelům zasílat informační bulletiny, oznámení a e-mailový marketing prostřednictvím naší odchozí služby SMTP. Také jsme přidali možnost nastavit kvóty úložiště pro celou doménu a pro alias pro IMAP/POP3/CalDAV.",
10332+
"Our service was founded in 2017 and powers email for over 500,000 domains – including notable users such as Canonical, Netflix, The Linux Foundation, several universities and governments, and more.": "Naše služba byla založena v roce 2017 a poskytuje e-mail pro více než 500 000 domén – včetně významných uživatelů, jako jsou Canonical, Netflix, The Linux Foundation, několik univerzit a vlád a další.",
10333+
"Storage maximum quota for aliases on this domain name. Enter a value such as \"1 GB\" that will be parsed by": "Maximální kvóta úložiště pro aliasy na tomto názvu domény. Zadejte hodnotu, například „1 GB“, podle které bude analyzován",
10334+
"bytes": "bajtů",
10335+
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Maximální kvóta úložiště pro tento alias. Ponechte prázdné, chcete-li obnovit aktuální maximální kvótu domény, nebo zadejte hodnotu, například „1 GB“, podle které bude analyzován",
10336+
". This value can only be adjusted by domain admins.": ". Tuto hodnotu mohou upravit pouze správci domény."
1032710337
}

locales/da.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -7311,5 +7311,15 @@
73117311
"Calendar already exists.": "Kalenderen findes allerede.",
73127312
"Canonical": "Kanonisk",
73137313
"Kubuntu": "Kubuntu",
7314-
"Lubuntu": "Lubuntu"
7314+
"Lubuntu": "Lubuntu",
7315+
"export format already supported).": "eksportformat allerede understøttet).",
7316+
"Webhook signature support": "Webhook signatur support",
7317+
"header was added.": "header blev tilføjet.",
7318+
"Bounce webhook support was added": "Bounce webhook-understøttelse blev tilføjet",
7319+
"and we now allow users to send newsletters, announcements, and email marketing through our outbound SMTP service. We also added the ability to set domain-wide and alias-specific storage quotas for IMAP/POP3/CalDAV.": "og vi tillader nu brugere at sende nyhedsbreve, meddelelser og e-mail-marketing gennem vores udgående SMTP-tjeneste. Vi tilføjede også muligheden for at indstille domænedækkende og aliasspecifikke lagerkvoter for IMAP/POP3/CalDAV.",
7320+
"Our service was founded in 2017 and powers email for over 500,000 domains – including notable users such as Canonical, Netflix, The Linux Foundation, several universities and governments, and more.": "Vores tjeneste blev grundlagt i 2017 og driver e-mail til over 500.000 domæner – herunder bemærkelsesværdige brugere som Canonical, Netflix, The Linux Foundation, adskillige universiteter og regeringer og mere.",
7321+
"Storage maximum quota for aliases on this domain name. Enter a value such as \"1 GB\" that will be parsed by": "Maksimal lagringskvote for aliaser på dette domænenavn. Indtast en værdi såsom \"1 GB\", som vil blive parset af",
7322+
"bytes": "bytes",
7323+
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Maksimal lagringskvote for dette alias. Lad stå tomt for at nulstille til domænets nuværende maksimale kvote, eller indtast en værdi såsom \"1 GB\", der vil blive parset af",
7324+
". This value can only be adjusted by domain admins.": ". Denne værdi kan kun justeres af domæneadministratorer."
73157325
}

locales/de.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -9362,5 +9362,15 @@
93629362
"Calendar already exists.": "Kalender existiert bereits.",
93639363
"Canonical": "Kanonisch",
93649364
"Kubuntu": "Kubuntu",
9365-
"Lubuntu": "Lubuntu"
9365+
"Lubuntu": "Lubuntu",
9366+
"export format already supported).": "Exportformat wird bereits unterstützt).",
9367+
"Webhook signature support": "Unterstützung für Webhook-Signaturen",
9368+
"header was added.": "Kopfzeile wurde hinzugefügt.",
9369+
"Bounce webhook support was added": "Bounce-Webhook-Unterstützung wurde hinzugefügt",
9370+
"and we now allow users to send newsletters, announcements, and email marketing through our outbound SMTP service. We also added the ability to set domain-wide and alias-specific storage quotas for IMAP/POP3/CalDAV.": "und wir ermöglichen Benutzern jetzt, Newsletter, Ankündigungen und E-Mail-Marketing über unseren ausgehenden SMTP-Dienst zu versenden. Wir haben auch die Möglichkeit hinzugefügt, domänenweite und aliasspezifische Speicherkontingente für IMAP/POP3/CalDAV festzulegen.",
9371+
"Our service was founded in 2017 and powers email for over 500,000 domains – including notable users such as Canonical, Netflix, The Linux Foundation, several universities and governments, and more.": "Unser Dienst wurde 2017 gegründet und unterstützt den E-Mail-Verkehr von über 500.000 Domänen – darunter namhafte Benutzer wie Canonical, Netflix, The Linux Foundation, mehrere Universitäten und Regierungen und mehr.",
9372+
"Storage maximum quota for aliases on this domain name. Enter a value such as \"1 GB\" that will be parsed by": "Maximales Speicherkontingent für Aliase für diesen Domänennamen. Geben Sie einen Wert wie „1 GB“ ein, der analysiert wird von",
9373+
"bytes": "Bytes",
9374+
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Maximales Speicherkontingent für diesen Alias. Lassen Sie das Feld leer, um das aktuelle maximale Kontingent der Domäne zurückzusetzen, oder geben Sie einen Wert wie „1 GB“ ein, der von",
9375+
". This value can only be adjusted by domain admins.": ". Dieser Wert kann nur von Domänenadministratoren angepasst werden."
93669376
}

locales/en.json

+12-1
Original file line numberDiff line numberDiff line change
@@ -6703,5 +6703,16 @@
67036703
"Calendar": "Calendar",
67046704
"Canonical": "Canonical",
67056705
"Kubuntu": "Kubuntu",
6706-
"Lubuntu": "Lubuntu"
6706+
"Lubuntu": "Lubuntu",
6707+
"export format already supported).": "export format already supported).",
6708+
"Webhook signature support": "Webhook signature support",
6709+
"header was added.": "header was added.",
6710+
"Bounce webhook support was added": "Bounce webhook support was added",
6711+
"and we now allow users to send newsletters, announcements, and email marketing through our outbound SMTP service. We also added the ability to set domain-wide and alias-specific storage quotas for IMAP/POP3/CalDAV.": "and we now allow users to send newsletters, announcements, and email marketing through our outbound SMTP service. We also added the ability to set domain-wide and alias-specific storage quotas for IMAP/POP3/CalDAV.",
6712+
"Our service was founded in 2017 and powers email for over 500,000 domains – including notable users such as Canonical, Netflix, The Linux Foundation, several universities and governments, and more.": "Our service was founded in 2017 and powers email for over 500,000 domains – including notable users such as Canonical, Netflix, The Linux Foundation, several universities and governments, and more.",
6713+
"Storage maximum quota for aliases on this domain name. Enter a value such as \"1 GB\" that will be parsed by": "Storage maximum quota for aliases on this domain name. Enter a value such as \"1 GB\" that will be parsed by",
6714+
"bytes": "bytes",
6715+
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by",
6716+
". This value can only be adjusted by domain admins.": ". This value can only be adjusted by domain admins.",
6717+
"Private Business Email Service": "Private Business Email Service"
67076718
}

0 commit comments

Comments
 (0)