Skip to content

Commit

Permalink
feat: seo and meta tags (#13)
Browse files Browse the repository at this point in the history
* fix: sync landing page meta information

* feat: add link rel=next element via pagination script

* feat: sync meta information for posts and pages

* fix: simplify default.njk title handling

* fix: excerpts for page and post meta tags, cleanup

* feat: sync tag meta info and simplify site title / org name handling

* fix: replace orgName with default site title from ghost settings setup

* fix: twitter:image meta

* feat: sync author meta info

* feat: sync search results meta info

* fix: sync robots.txt

* feat: start building sitemap, serve RSS feed and redirect with browsersync

* fix: remove ghost logo

* feat: build basic sitemaps

* fix: get sitemap fetcher working

* fix: use sitemap fetcher for all sitemaps

* fix: move comment outside of title element

* fix: remove xml package

* fix: use sitemap fetcher for posts
  • Loading branch information
scissorsneedfoodtoo authored Aug 13, 2021
1 parent 731726b commit 11eb377
Show file tree
Hide file tree
Showing 29 changed files with 513 additions and 167 deletions.
91 changes: 81 additions & 10 deletions .eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const dayjs = require("./utils/dayjs");
const cacheBuster = require("@mightyplow/eleventy-plugin-cache-buster");
const { settings } = require('./utils/ghost-settings');
const { escape } = require('lodash');
const fetch = require('node-fetch');
const xml2js = require('xml2js');

module.exports = function(config) {
// Minify HTML
Expand Down Expand Up @@ -149,11 +151,17 @@ module.exports = function(config) {
// Format dates for RSS feed
const buildDateFormatterShortcode = dateStr => {
const dateObj = dateStr ? new Date(dateStr) : new Date();
return dateObj.toUTCString();
return dayjs.tz(dateObj).locale('en').format('ddd, DD MMM YYYY HH:mm:ss ZZ');
}

config.addNunjucksShortcode("buildDateFormatter", buildDateFormatterShortcode);

const utcDateFormatterShortcode = dateStr => {
return dayjs.utc(dateStr).format();
}

config.addNunjucksShortcode("utcDateFormatter", utcDateFormatterShortcode);

config.addFilter("commentsEnabled", tagsArr => {
return !tagsArr.map(tag => tag.name).includes('#disable-comments');
});
Expand All @@ -169,6 +177,9 @@ module.exports = function(config) {
.replace(/'/g, ''')
.replace(/`/g, '`')
.replace(/=/g, '=');

// Simpler escaping for sitemaps
const partialEscaper = s => escape(s);

config.addNunjucksShortcode("fullEscaper", fullEscaper);

Expand Down Expand Up @@ -210,10 +221,6 @@ module.exports = function(config) {
}
const returnData = {...baseData};

// Would probably be better to look up image dimensions in ghost.js,
// to prevent looking up the same dimensions for each author image.
// Could also keep a map of article or page feature_images, if we want
// to calculate all that there, too
const createImageObj = (url, obj) => {
let { width, height } = obj;

Expand Down Expand Up @@ -309,21 +316,85 @@ module.exports = function(config) {

config.addNunjucksAsyncShortcode("createJsonLd", createJsonLdShortcode);

const createExcerptShortcode = (excerpt) => {
return excerpt.replace(/\n+/g, ' ').split(' ').slice(0, 50).join(' ');
}

config.addNunjucksShortcode("createExcerpt", createExcerptShortcode);

const sitemapFetcherShortcode = async (page) => {
const apiUrl = process.env.GHOST_API_URL;
// will need some sort of map to handle all locales
const url = page === 'index' ?
`${apiUrl}/sitemap.xml` :
`${apiUrl}/sitemap-${page}.xml`;

const ghostXml = await fetch(url)
.then(res => res.text())
.then(res => res)
.catch(err => console.log(err));

const parser = new xml2js.Parser();
const ghostXmlObj = await parser.parseStringPromise(ghostXml)
.then((res) => res)
.catch((err) => console.log(err));

const target = page === 'index' ?
ghostXmlObj.sitemapindex.sitemap :
ghostXmlObj.urlset.url;

const urlSwapper = url => url.replace(apiUrl, process.env.SITE_URL);

let xmlStr = target.reduce((acc, curr) => {
const wrapper = page === 'index' ? 'sitemap' : 'url';

acc += `
<${wrapper}>
<loc>${urlSwapper(curr.loc[0])}</loc>
<lastmod>${curr.lastmod[0]}</lastmod>
${curr['image:image'] ? `
<image:image>
<image:loc>${partialEscaper(urlSwapper(curr['image:image'][0]['image:loc'][0]))}</image:loc>
<image:caption>${partialEscaper(curr['image:image'][0]['image:caption'][0])}</image:caption>
</image:image>` : ''
}
</${wrapper}>`;

return acc;
}, '');

// xmlStr = xmlStr.replace(/\s+/g, '');

// To do: minify xml after build
return xmlStr;
}

config.addNunjucksAsyncShortcode("sitemapFetcher", sitemapFetcherShortcode);

// Don't ignore the same files ignored in the git repo
config.setUseGitIgnore(false);

// Display 404 page in BrowserSnyc
// Display 404 and RSS pages in BrowserSnyc
config.setBrowserSyncConfig({
callbacks: {
ready: (err, bs) => {
const content_404 = readFileSync("dist/404.html");
const content_RSS = readFileSync("dist/rss.xml");

bs.addMiddleware("*", (req, res) => {
res.writeHead(404, { "Content-Type": "text/html; charset=UTF-8" });
if (req.url.match(/^\/rss\/?$/)) {
res.writeHead(302, { "Content-Type": "text/xml; charset=UTF-8" });

// Provides the RSS feed content without redirect
res.write(content_RSS);
res.end();
} else {
res.writeHead(404, { "Content-Type": "text/html; charset=UTF-8" });

// Provides the 404 content without redirect.
res.write(content_404);
res.end();
// Provides the 404 content without redirect
res.write(content_404);
res.end();
}
});
}
}
Expand Down
5 changes: 5 additions & 0 deletions i18n/locales/en/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More",
"keywords": "freeCodeCamp, programming, front-end, programmer, article, regular expressions, Python, JavaScript, AWS, JSON, HTML, CSS, Bootstrap, React, Vue, Webpack",
"description": "Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice."
}
1 change: 0 additions & 1 deletion i18n/locales/en/translations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"keyword-meta": "freeCodeCamp, programming, front-end, programmer, article, regular expressions, Python, JavaScript, AWS, JSON, HTML, CSS, Bootstrap, React, Vue, Webpack",
"buttons": {
"forum": "Forum",
"donate": "Donate",
Expand Down
5 changes: 5 additions & 0 deletions i18n/locales/es/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Cursos de programación freeCodeCamp en Español: Python, JavaScript, Git y más",
"keywords": "freeCodeCamp, programación, front-end, programador, artículo, expresiones regulares, Python, JavaScript, AWS, JSON, HTML, CSS, Bootstrap, React, Vue, Webpack",
"description": "Descubre miles de cursos de programación escritos por expertos. Aprende Desarrollo Web, Ciencia de Datos, DevOps, Seguridad y obtén asesoramiento profesional para desarrolladores."
}
1 change: 0 additions & 1 deletion i18n/locales/es/translations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"keyword-meta": "freeCodeCamp, programación, front-end, programador, artículo, expresiones regulares, Python, JavaScript, AWS, JSON, HTML, CSS, Bootstrap, React, Vue, Webpack",
"buttons": {
"forum": "Foro",
"donate": "Donar",
Expand Down
5 changes: 5 additions & 0 deletions i18n/locales/zh/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "freeCodeCamp 中文编程教程:Python、JavaScript、Java、Git 等",
"keywords": "freeCodeCamp, freeCodeCamp中文, 编程, 前端, 程序员, Python, JavaScript, Git, AWS, JSON, HTML, CSS, Bootstrap, React, Vue",
"description": "freeCodeCamp 是一个免费学习编程的开发者社区,涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等,还有活跃的技术论坛和丰富的社区活动,在你学习编程和找工作时为你提供建议和帮助。"
}
1 change: 0 additions & 1 deletion i18n/locales/zh/translations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"keyword-meta": "freeCodeCamp, freeCodeCamp中文, 编程, 前端, 程序员, Python, JavaScript, Git, AWS, JSON, HTML, CSS, Bootstrap, React, Vue",
"buttons": {
"forum": "论坛",
"donate": "捐款",
Expand Down
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
"eslint": "7.32.0",
"eslint-plugin-ghost": "2.6.0",
"lodash": "4.17.21",
"probe-image-size": "7.2.1"
"node-fetch": "^2.6.1",
"probe-image-size": "7.2.1",
"xml2js": "^0.4.23"
}
}
17 changes: 15 additions & 2 deletions src/_includes/assets/js/pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@

const fetchNextPage = async () => {
try {
const res = await fetch(`${window.location.href}/${nextPageNum}/`);
nextPageNum++;
const nextPageUrl = `${window.location.href}${nextPageNum}/`;
const res = await fetch(nextPageUrl);

if (res.ok) {
const text = await res.text();
const parser = new DOMParser();
const nextTag = document.querySelector('link[rel="next"]');

if (nextTag) {
nextTag.href = nextPageUrl;
} else {
const head = document.getElementsByTagName('head')[0];
const link = document.createElement('link');
link.rel = 'next';
link.href = nextPageUrl;

head.appendChild(link);
}

nextPageNum++;
return await parser.parseFromString(text, 'text/html');
} else {
readMoreBtn.style.display = 'none';
Expand Down
46 changes: 23 additions & 23 deletions src/_includes/layouts/author.njk
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends 'layouts/default.njk' %}
{% from "partials/card.njk" import card %}

{% set title = author.name + " " + site.title %}
{% set title = author.name + " - " + site.title %}

{% block content %}
{% include "partials/author-info.njk" %}
Expand All @@ -21,31 +21,31 @@
{% endblock %}

{%- block seo -%}
<!-- Facebook OpenGraph -->
<meta property="og:site_name" content="{{ site.title }}">
<meta property="og:type" content="profile">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{% t 'meta:description' %}">
<meta property="og:url" content="{{ site.url + author.path }}">
<meta property="og:image" content="{{ author.cover_image if author.cover_image else site.cover_image }}">
<meta property="article:publisher" content="https://www.facebook.com/freecodecamp">
{% if post.primary_author.facebook %}
<meta property="article:author" content="{{ post.primary_author.facebook }}">
{% endif %}

<!--Twitter Card-->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ title }}">
<meta property="twitter:description" content="{% t 'meta:description' %}">
<meta name="twitter:url" content="{{ site.url + author.path }}">
<meta property="twitter:image" content="{{ author.cover_image if author.cover_image else site.cover_image }}">
<meta name="twitter:site" content="{{ site.twitter }}">
<meta name="twitter:creator" content="{{ author.twitter }}">
<meta name="twitter:card" content="summary">
<meta name="twitter:url" content="{{ site.url + page.url }}">
<meta name="twitter:title" content="{{ title }} – {{ site.title }}">
<meta name="twitter:description" content="{{ content }}">
<meta name="twitter:image" content="{{ author.profile_image }}">
{% if author.twitter %}
<meta name="twitter:creator" content="{{ post.primary_author.twitter }}">
{% endif %}

<!--Schema-->
<link rel="author" href="{{ author.website }}">
<link rel="publisher" href="{{ site.url }}">
<meta itemprop="name" content="{{ title }} – {{ site.title }}">
<meta itemprop="description" content="{{ content }}">
<meta itemprop="image" content="{{ author.profile_image }}">

<!-- Facebook OpenGraph -->
<meta property="og:url" content="{{ site.url + page.url }}">
<meta property="og:type" content="website">
<meta property="og:title" content="{{ title }} – {{ site.title }}">
<meta property="og:image" content="{{ author.profile_image }}">
<meta property="og:description" content="{{ content }}">
<meta property="og:site_name" content="{{ site.title }}">
<meta property="og:locale" content="{{ site.lang }}">
<meta property="article:author" content="{{ site.url }}">
<meta property="og:image:width" content="{{ author.image_dimensions.cover_image.width if author.cover_image else site.image_dimensions.cover_image.width }}">
<meta property="og:image:height" content="{{ author.image_dimensions.cover_image.height if author.cover_image else site.image_dimensions.cover_image.height }}">
{%- endblock -%}

{% block jsonLd %}
Expand Down
Loading

0 comments on commit 11eb377

Please sign in to comment.