Skip to content

Commit 50e292d

Browse files
committed
fix: disabling bot crawling on staging and refactor sitemap to add dates and images
1 parent 36285e3 commit 50e292d

File tree

10 files changed

+127
-66
lines changed

10 files changed

+127
-66
lines changed

public/robots.txt

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getEnv } from '@/helpers/getEnvHelper';
33
export const IS_SSR = import.meta.env?.SSR ?? false;
44
export const IS_PRERENDER = import.meta.env?.MODE === 'prerender';
55
export const HOST_URL = getEnv<string>('VITE_HOST_URL') || 'https://blog.eleven-labs.com';
6+
export const IS_ENV_PRODUCTION = HOST_URL === 'https://blog.eleven-labs.com';
67
export const BASE_URL = import.meta.env?.BASE_URL || '/';
78

89
export const IS_DEBUG = getEnv<string>('VITE_IS_DEBUG') === 'true';

src/helpers/assetHelper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const getCoverPath = ({
2424
device: DeviceEnum;
2525
pixelRatio: number;
2626
}): string => {
27-
const isProd: boolean = process.env.NODE_ENV === 'production';
27+
const isProd: boolean = true; //process.env.NODE_ENV === 'production';
2828
const directoryPath = dirname(path);
2929
const filename = basename(path, extname(path));
3030
const imageFormat = IMAGE_FORMATS[device][format];
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { writeFileSync } from 'node:fs';
2+
import { resolve } from 'node:path';
3+
4+
import { HOST_URL, IS_ENV_PRODUCTION } from '@/constants';
5+
6+
export const getRobotsTxt = (): string => {
7+
return (
8+
IS_ENV_PRODUCTION
9+
? ['User-agent: *', 'Allow: /', `Sitemap: ${HOST_URL}/sitemap.xml`]
10+
: ['User-agent: *', 'Disallow: /']
11+
).join('\n');
12+
};
13+
14+
export const generateRobotsTxt = async (options: { rootDir: string }): Promise<void> => {
15+
const robotsTxt = getRobotsTxt();
16+
writeFileSync(resolve(options.rootDir, 'robots.txt'), robotsTxt, 'utf8');
17+
};

src/helpers/prerenderHelper/generateSitemap.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,32 @@ import * as xml2js from 'xml2js';
44

55
import { blogUrl } from '@/config/website';
66
import { DEFAULT_LANGUAGE } from '@/constants';
7+
import type { SitemapEntry } from '@/helpers/prerenderHelper/getSitemapEntries';
78

8-
export const getSitemap = (
9-
sitemapEntries: { links: { lang: string; url: string }[]; changefreq?: string; priority?: number }[]
10-
): string => {
9+
export const getSitemap = (sitemapEntries: SitemapEntry[]): string => {
1110
const builder = new xml2js.Builder();
1211
return builder.buildObject({
1312
urlset: {
1413
$: {
1514
xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9',
1615
'xmlns:xhtml': 'http://www.w3.org/1999/xhtml',
17-
'xmlns:news': 'http://www.google.com/schemas/sitemap-news/0.9',
16+
'xmlns:image': 'http://www.google.com/schemas/sitemap-image/1.1',
1817
},
19-
url: sitemapEntries.map(({ links, priority, changefreq }) => {
18+
url: sitemapEntries.map(({ links, priority, changeFrequency, lastModified, image }) => {
2019
const defaultLink = links.find((link) => link.lang === DEFAULT_LANGUAGE) ?? links[0];
2120
return {
2221
loc: `${blogUrl}${defaultLink.url}`,
23-
...(changefreq ? { changefreq } : {}),
24-
priority: priority?.toFixed(1) ?? 0.3,
22+
...(lastModified ? { lastmod: lastModified } : {}),
23+
...(changeFrequency ? { changefreq: changeFrequency } : {}),
24+
...(priority ? { priority } : {}),
25+
...(image
26+
? {
27+
'image:image': {
28+
'image:loc': `${blogUrl}${image.url}`,
29+
...(image.description ? { 'image:caption': image.description } : {}),
30+
},
31+
}
32+
: {}),
2533
...(links.length > 1
2634
? {
2735
'xhtml:link': links.map((link) => ({

src/helpers/prerenderHelper/getSitemapEntries.test.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getSitemapEntries } from './getSitemapEntries';
1+
import { getSitemapEntries, SitemapEntry } from './getSitemapEntries';
22

33
describe('getSitemapEntries', () => {
44
test('should generate sitemap entries correctly', () => {
@@ -12,38 +12,53 @@ describe('getSitemapEntries', () => {
1212
};
1313
});
1414
vi.mock('@/helpers/markdownContentManagerHelper', () => ({
15-
getPosts: (): { lang: string; slug: string; categories: string[]; authors: string[] }[] => [
16-
{ lang: 'fr', slug: 'post-1', categories: ['architecture'], authors: ['author-1'] },
17-
{ lang: 'en', slug: 'post-2', categories: ['php'], authors: ['author-1'] },
15+
getPosts: (): {
16+
lang: string;
17+
slug: string;
18+
categories: string[];
19+
authors: string[];
20+
date: string;
21+
cover?: { path: string };
22+
}[] => [
23+
{
24+
lang: 'fr',
25+
slug: 'post-1',
26+
categories: ['architecture'],
27+
authors: ['author-1'],
28+
date: '2024-01-01T00:00:00',
29+
cover: { path: '/imgs/post-1/cover.png' },
30+
},
31+
{ lang: 'en', slug: 'post-2', categories: ['php'], authors: ['author-1'], date: '2024-01-01T00:00:00' },
1832
],
1933
getAuthors: (): { username: string }[] => [{ username: 'author-1' }],
2034
}));
2135

2236
// Expected result
23-
const expectedSitemapEntries = [
24-
{ priority: 1, links: [{ lang: 'fr', url: '/fr/post-1/' }] },
25-
{ priority: 1, links: [{ lang: 'en', url: '/en/post-2/' }] },
37+
const expectedSitemapEntries: SitemapEntry[] = [
2638
{
27-
priority: 0.8,
28-
changefreq: 'weekly',
39+
links: [{ lang: 'fr', url: '/fr/post-1/' }],
40+
lastModified: '2024-01-01T00:00:00',
41+
image: { url: '/imgs/post-1/cover-w400-h245-x2.avif' },
42+
},
43+
{ links: [{ lang: 'en', url: '/en/post-2/' }], lastModified: '2024-01-01T00:00:00' },
44+
{
45+
changeFrequency: 'weekly',
2946
links: [
3047
{ lang: 'fr', url: '/' },
3148
{ lang: 'fr', url: '/fr/' },
3249
{ lang: 'en', url: '/en/' },
3350
],
3451
},
3552
{
36-
priority: 0.7,
37-
changefreq: 'weekly',
53+
changeFrequency: 'weekly',
3854
links: [
3955
{ lang: 'fr', url: '/fr/categories/all/' },
4056
{ lang: 'en', url: '/en/categories/all/' },
4157
],
4258
},
43-
{ priority: 0.7, changefreq: 'weekly', links: [{ lang: 'en', url: '/en/categories/php/' }] },
44-
{ priority: 0.7, changefreq: 'weekly', links: [{ lang: 'fr', url: '/fr/categories/architecture/' }] },
59+
{ changeFrequency: 'weekly', links: [{ lang: 'en', url: '/en/categories/php/' }] },
60+
{ changeFrequency: 'weekly', links: [{ lang: 'fr', url: '/fr/categories/architecture/' }] },
4561
{
46-
priority: 0.5,
4762
links: [
4863
{ lang: 'fr', url: '/fr/authors/author-1/' },
4964
{ lang: 'en', url: '/en/authors/author-1/' },
Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,73 @@
1+
import { DeviceEnum, ImageFormatEnum, PATHS } from '@/constants';
2+
import { getCoverPath } from '@/helpers/assetHelper';
13
import { getAuthors, getPosts } from '@/helpers/markdownContentManagerHelper';
24
import {
35
getAuthorPageUrls,
46
getCategoryPageUrls,
57
getHomePageUrls,
6-
getPostPageUrls,
78
getTutorialStepPageUrls,
89
} from '@/helpers/prerenderHelper/getUrls';
10+
import { generatePath } from '@/helpers/routerHelper';
911

10-
type Link = {
11-
lang: string;
12-
url: string;
13-
};
14-
15-
type SitemapEntry = {
16-
links: Link[];
17-
changefreq?: string;
18-
priority: number;
19-
};
12+
export interface SitemapEntry {
13+
links: {
14+
lang: string;
15+
url: string;
16+
}[];
17+
image?: {
18+
url: string;
19+
description?: string;
20+
};
21+
lastModified?: string;
22+
changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
23+
priority?: number;
24+
}
2025

2126
export const getSitemapEntries = (): SitemapEntry[] => {
2227
const posts = getPosts();
2328
const authors = getAuthors();
2429

2530
const rootEntry: SitemapEntry = {
26-
priority: 0.8,
2731
links: getHomePageUrls(),
28-
changefreq: 'weekly',
32+
changeFrequency: 'weekly',
2933
};
3034

3135
const categoryPageUrls = getCategoryPageUrls(posts);
3236
const categoryEntries: SitemapEntry[] = categoryPageUrls.map((urls) => ({
33-
priority: 0.7,
3437
links: urls,
35-
changefreq: 'weekly',
38+
changeFrequency: 'weekly',
3639
}));
3740

3841
const authorPageUrls = getAuthorPageUrls(posts, authors);
3942
const authorEntries: SitemapEntry[] = authorPageUrls.map((urls) => ({
40-
priority: 0.5,
4143
links: urls,
4244
}));
4345

44-
const postPageUrls = getPostPageUrls(posts);
45-
const postEntries: SitemapEntry[] = postPageUrls.map((urls) => ({
46-
priority: 1,
47-
links: urls,
46+
const postEntries: SitemapEntry[] = posts.map((post) => ({
47+
links: [
48+
{
49+
lang: post.lang,
50+
url: generatePath(PATHS.POST, { lang: post.lang, slug: post.slug }),
51+
},
52+
],
53+
lastModified: post.date,
54+
image: post.cover?.path
55+
? {
56+
url: getCoverPath({
57+
path: post.cover?.path,
58+
format: ImageFormatEnum.HIGHLIGHTED_TUTORIAL_POST_CARD_COVER,
59+
pixelRatio: 2,
60+
device: DeviceEnum.DESKTOP,
61+
}),
62+
description: post.cover?.alt,
63+
}
64+
: undefined,
4865
}));
4966

5067
const tutorialStepUrls = getTutorialStepPageUrls(posts);
5168
const tutorialStepEntries: SitemapEntry[] = tutorialStepUrls.map((urls) => ({
52-
priority: 0.9,
5369
links: urls,
5470
}));
5571

56-
return [rootEntry, ...categoryEntries, ...authorEntries, ...postEntries, ...tutorialStepEntries].sort(
57-
(a, b) => b?.priority - a?.priority
58-
);
72+
return [...postEntries, rootEntry, ...categoryEntries, ...authorEntries, ...tutorialStepEntries];
5973
};

src/helpers/prerenderHelper/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { resolve } from 'node:path';
22

33
import { DEFAULT_LANGUAGE, LANGUAGES_AVAILABLE, PATHS } from '@/constants';
4+
import { generateRobotsTxt } from '@/helpers/prerenderHelper/generateRobotsTxt';
45
import { generatePath } from '@/helpers/routerHelper';
56

67
import { generateFeedFile } from './generateFeedFile';
@@ -41,6 +42,9 @@ export const generateFiles = async (options: { rootDir: string; baseUrl: string
4142
rootDir: __dirname,
4243
sitemapEntries,
4344
}),
45+
generateRobotsTxt({
46+
rootDir: __dirname,
47+
}),
4448
]);
4549
generateFeedFile({ rootDir: __dirname });
4650

src/middlewares/imageMiddleware.ts renamed to src/requestHandlers/imageHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Request, Response } from 'express';
1+
import type { RequestHandler } from 'express';
22
import mime from 'mime';
33
import { existsSync, readFileSync } from 'node:fs';
44
import { resolve } from 'node:path';
@@ -14,7 +14,7 @@ const contentTypesByFormat = {
1414

1515
type FormatEnum = keyof typeof contentTypesByFormat;
1616

17-
export const imageMiddleware = async (req: Request, res: Response): Promise<unknown> => {
17+
export const imageHandler: RequestHandler = async (req, res) => {
1818
try {
1919
const imagePath = resolve(process.cwd(), 'public', req.path.slice(1) as string);
2020
if (!existsSync(imagePath)) {

src/server.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import chokidar from 'chokidar';
2-
import express from 'express';
2+
import express, { RequestHandler } from 'express';
33
import i18next from 'i18next';
44
import i18nextHttpMiddleware from 'i18next-http-middleware';
55
import { cpSync, statSync } from 'node:fs';
@@ -12,13 +12,25 @@ import { i18nResources } from '@/config/i18n/i18nResources';
1212
import { BASE_URL } from '@/constants';
1313
import { writeJsonDataFiles } from '@/helpers/contentHelper';
1414
import { loadDataByMarkdownFilePath } from '@/helpers/markdownContentManagerHelper';
15+
import { getRobotsTxt } from '@/helpers/prerenderHelper/generateRobotsTxt';
1516
import { getSitemap } from '@/helpers/prerenderHelper/generateSitemap';
1617
import { getSitemapEntries } from '@/helpers/prerenderHelper/getSitemapEntries';
1718
import { createRequestByExpressRequest } from '@/helpers/requestHelper';
18-
import { imageMiddleware } from '@/middlewares/imageMiddleware';
19+
import { imageHandler } from '@/requestHandlers/imageHandler';
1920

2021
const isProd: boolean = process.env.NODE_ENV === 'production';
2122

23+
const robotsTxtHandler: RequestHandler = (_, res) => {
24+
const robotsTxt = getRobotsTxt();
25+
res.status(200).set({ 'Content-Type': 'text/plain' }).end(robotsTxt);
26+
};
27+
28+
const sitemapHandler: RequestHandler = (_, res) => {
29+
const sitemapEntries = getSitemapEntries();
30+
const sitemap = getSitemap(sitemapEntries);
31+
res.status(200).set({ 'Content-Type': 'text/xml' }).end(sitemap);
32+
};
33+
2234
const createServer = async (): Promise<void> => {
2335
i18next.use(i18nextHttpMiddleware.LanguageDetector).init({
2436
...i18nConfig,
@@ -41,13 +53,10 @@ const createServer = async (): Promise<void> => {
4153
dirname: __dirname,
4254
});
4355

56+
app.get(/\/imgs\//, imageHandler);
4457
app.use(BASE_URL, serveStatic(__dirname, { index: false }));
45-
46-
app.get('/sitemap.xml', (_, res) => {
47-
const sitemapEntries = getSitemapEntries();
48-
const sitemap = getSitemap(sitemapEntries);
49-
res.status(200).set({ 'Content-Type': 'text/xml' }).end(sitemap);
50-
});
58+
app.get('/robots.txt', robotsTxtHandler);
59+
app.get('/sitemap.xml', sitemapHandler);
5160

5261
app.use('*', async (req, res, next) => {
5362
try {
@@ -93,14 +102,10 @@ const createServer = async (): Promise<void> => {
93102
});
94103
});
95104

96-
app.get(/\/imgs\//, imageMiddleware);
105+
app.get(/\/imgs\//, imageHandler);
97106
app.use(vite.middlewares);
98-
99-
app.get('/sitemap.xml', (_, res) => {
100-
const sitemapEntries = getSitemapEntries();
101-
const sitemap = getSitemap(sitemapEntries);
102-
res.status(200).set({ 'Content-Type': 'text/xml' }).end(sitemap);
103-
});
107+
app.get('/robots.txt', robotsTxtHandler);
108+
app.get('/sitemap.xml', sitemapHandler);
104109

105110
app.use('*', async (req, res, next) => {
106111
const url = req.originalUrl;

0 commit comments

Comments
 (0)