Compare commits
155 Commits
4.0
...
feat/types
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e61cfdc7 | ||
|
|
47a8f96ddc | ||
|
|
16247f755a | ||
|
|
ff16ea542f | ||
|
|
e3adcdaba4 | ||
|
|
6783d7ea44 | ||
|
|
f06dfce7fb | ||
|
|
3df8db5fa5 | ||
|
|
5c92cdedd8 | ||
|
|
07b6d067c4 | ||
|
|
a7f9e7d735 | ||
|
|
3521525611 | ||
|
|
43260ff14f | ||
|
|
102ccc6a6b | ||
|
|
415dc2d8e8 | ||
|
|
e0e6168cfe | ||
|
|
dd7c0ec003 | ||
|
|
190c75cebe | ||
|
|
813a3d9b2b | ||
|
|
c2dda3bc35 | ||
|
|
4711ab9a6f | ||
|
|
5f2836a148 | ||
|
|
badb2c029d | ||
|
|
8a24b3e695 | ||
|
|
8b3f8ee6b8 | ||
|
|
f9db9bee95 | ||
|
|
e8d2bd00c6 | ||
|
|
f4e505113c | ||
|
|
f675f08d83 | ||
|
|
a12ec64af5 | ||
|
|
24512374e8 | ||
|
|
359f5d6a4d | ||
|
|
c7302d7484 | ||
|
|
6ab477df8d | ||
|
|
961d00e70e | ||
|
|
c1a53cf3cc | ||
|
|
1f485c21f7 | ||
|
|
e886d0bacb | ||
|
|
8a07f2f685 | ||
|
|
19ad916334 | ||
|
|
b30016b6f4 | ||
|
|
57395f769a | ||
|
|
b91c11b273 | ||
|
|
c026f9c928 | ||
|
|
aee51ee43e | ||
|
|
3b12130579 | ||
|
|
3dd9429338 | ||
|
|
0af54cd906 | ||
|
|
750e6e5a36 | ||
|
|
5b93bc42db | ||
|
|
8b32a3a831 | ||
|
|
a28204c908 | ||
|
|
4aca07e3d4 | ||
|
|
5c2562dadb | ||
|
|
e934dc60f4 | ||
|
|
ad4f35764d | ||
|
|
a715a85b46 | ||
|
|
f16a207e7c | ||
|
|
6582d65935 | ||
|
|
ab36350cdc | ||
|
|
3b05a615d8 | ||
|
|
9a2bc75646 | ||
|
|
d283ce7c67 | ||
|
|
59ed243fa7 | ||
|
|
ca35551e4f | ||
|
|
cab06b46da | ||
|
|
f5e980d8ec | ||
|
|
6187b1dc52 | ||
|
|
a3b8b5653a | ||
|
|
8f8e2f41d8 | ||
|
|
89a436a5b7 | ||
|
|
231e295f01 | ||
|
|
64e20e9fc1 | ||
|
|
621f841fbf | ||
|
|
c61afb15bc | ||
|
|
595f3680be | ||
|
|
ee65c56bf3 | ||
|
|
a2c339f2d5 | ||
|
|
a3031a2371 | ||
|
|
952169ec8e | ||
|
|
fbd82ce215 | ||
|
|
35f61e876e | ||
|
|
bb9878fdb7 | ||
|
|
ee843cc9e2 | ||
|
|
cbd79ef299 | ||
|
|
af9e266190 | ||
|
|
0cbd401071 | ||
|
|
0929d40bd0 | ||
|
|
927aa0a066 | ||
|
|
85eff7f894 | ||
|
|
11695f4b05 | ||
|
|
aebee9b3a3 | ||
|
|
6b52baf093 | ||
|
|
6922fd826f | ||
|
|
ec29e1836e | ||
|
|
dca9eb32cd | ||
|
|
4b681c6317 | ||
|
|
9c24ff23e3 | ||
|
|
cdc87a99e1 | ||
|
|
ea16e99598 | ||
|
|
ba86e8a6b1 | ||
|
|
5f23d4c7eb | ||
|
|
8264c4509f | ||
|
|
6c8aea98da | ||
|
|
64ccd02d53 | ||
|
|
f8c1c6278b | ||
|
|
4786265e04 | ||
|
|
8badf383b2 | ||
|
|
c4406b7649 | ||
|
|
1e878069bc | ||
|
|
8234de2b8c | ||
|
|
3466708ed4 | ||
|
|
b440fd9787 | ||
|
|
9bc73ab738 | ||
|
|
91c16a5e32 | ||
|
|
1768150fb1 | ||
|
|
4a1374c978 | ||
|
|
43df31b312 | ||
|
|
2037edb2da | ||
|
|
de8a4d4acf | ||
|
|
a67a27299e | ||
|
|
5d164198d4 | ||
|
|
a76b9d9ac0 | ||
|
|
6ed83349ba | ||
|
|
1b21550e48 | ||
|
|
c1d0ff7ea2 | ||
|
|
26125fc6d7 | ||
|
|
aff7d8eece | ||
|
|
fd939f198a | ||
|
|
18e4804a51 | ||
|
|
ed8bf11150 | ||
|
|
61f088d42a | ||
|
|
faee01b22d | ||
|
|
dc56ef6190 | ||
|
|
f393a23994 | ||
|
|
8e61330080 | ||
|
|
2c18529429 | ||
|
|
88ff836bfb | ||
|
|
66cb4f9a06 | ||
|
|
d9697b74fd | ||
|
|
863b7fa08b | ||
|
|
64078f9d1a | ||
|
|
5f8ead3d2f | ||
|
|
cb16abc8e1 | ||
|
|
52d00a0654 | ||
|
|
d5495f7280 | ||
|
|
564c9fdd4f | ||
|
|
e75df0ef9e | ||
|
|
642cbbf6d3 | ||
|
|
032602ad3b | ||
|
|
522f16957a | ||
|
|
1f3bf761cd | ||
|
|
f76f0ea1a6 | ||
|
|
a40457edc8 | ||
|
|
076db6dd0a |
21
.github/workflows/aws-costs.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Sends Daily AWS Costs to Slack
|
||||
on:
|
||||
# Allow manual Run
|
||||
workflow_dispatch:
|
||||
# Run at 7:00 UTC every day
|
||||
schedule:
|
||||
- cron: "0 7 * * *"
|
||||
jobs:
|
||||
aws_costs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Costs
|
||||
env:
|
||||
AWS_KEY: ${{ secrets.COST_AWS_ACCESS_KEY }}
|
||||
AWS_SECRET: ${{ secrets.COST_AWS_SECRET_KEY }}
|
||||
AWS_REGION: ${{ secrets.COST_AWS_REGION }}
|
||||
SLACK_CHANNEL: ${{ secrets.SLACK_COST_CHANNEL }}
|
||||
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
|
||||
run: |
|
||||
npm install -g aws-cost-cli
|
||||
aws-cost -k $AWS_KEY -s $AWS_SECRET -r $AWS_REGION -S $SLACK_TOKEN -C $SLACK_CHANNEL
|
||||
4
.gitignore
vendored
@@ -20,3 +20,7 @@ pnpm-debug.log*
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
tests-examples
|
||||
@@ -9,6 +9,9 @@ import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
export default defineConfig({
|
||||
site: 'https://roadmap.sh',
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: 'dracula',
|
||||
},
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypeExternalLinks,
|
||||
@@ -18,6 +21,9 @@ export default defineConfig({
|
||||
],
|
||||
],
|
||||
},
|
||||
build: {
|
||||
format: 'file',
|
||||
},
|
||||
integrations: [
|
||||
tailwind({
|
||||
config: {
|
||||
|
||||
144
bin/best-practice-content.cjs
Normal file
@@ -0,0 +1,144 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the best-practices
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(__dirname, '../src/best-practices');
|
||||
const bestPracticeId = process.argv[2];
|
||||
|
||||
const allowedBestPracticeId = fs.readdirSync(BEST_PRACTICE_CONTENT_DIR);
|
||||
if (!bestPracticeId) {
|
||||
console.error('bestPractice is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedBestPracticeId.includes(bestPracticeId)) {
|
||||
console.error(`Invalid best practice key ${bestPracticeId}`);
|
||||
console.error(`Allowed keys are ${allowedBestPracticeId.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the best parctice content files
|
||||
const bestPracticeDirName = fs
|
||||
.readdirSync(BEST_PRACTICE_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === bestPracticeId);
|
||||
|
||||
if (!bestPracticeDirName) {
|
||||
console.error('Best practice directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bestPracticeDirPath = path.join(BEST_PRACTICE_CONTENT_DIR, bestPracticeDirName);
|
||||
const bestPracticeContentDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If best practice content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(bestPracticeContentDirPath)) {
|
||||
console.error(`Best Practice content already exists @ ${bestPracticeContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || controlName.startsWith('check:') || controlName.startsWith('ext_link:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlName.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree };
|
||||
}
|
||||
|
||||
const bestPractice = require(path.join(__dirname, `../public/jsons/best-practices/${bestPracticeId}`));
|
||||
const controls = bestPractice.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating
|
||||
const dirTree = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(bestPracticeContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
filePaths
|
||||
);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(bestPracticeContentDirPath, dirTree);
|
||||
console.log('Created best practice content directory structure');
|
||||
@@ -2,13 +2,18 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const jsonsDir = path.join(process.cwd(), 'public/jsons');
|
||||
const jsonFiles = fs.readdirSync(jsonsDir);
|
||||
const childJsonDirs = fs.readdirSync(jsonsDir);
|
||||
|
||||
jsonFiles.forEach((jsonFileName) => {
|
||||
console.log(`Compressing ${jsonFileName}...`);
|
||||
childJsonDirs.forEach((childJsonDir) => {
|
||||
const fullChildJsonDirPath = path.join(jsonsDir, childJsonDir);
|
||||
const jsonFiles = fs.readdirSync(fullChildJsonDirPath);
|
||||
|
||||
const jsonFilePath = path.join(jsonsDir, jsonFileName);
|
||||
const json = require(jsonFilePath);
|
||||
jsonFiles.forEach((jsonFileName) => {
|
||||
console.log(`Compressing ${jsonFileName}...`);
|
||||
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(json));
|
||||
const jsonFilePath = path.join(fullChildJsonDirPath, jsonFileName);
|
||||
const json = require(jsonFilePath);
|
||||
|
||||
fs.writeFileSync(jsonFilePath, JSON.stringify(json));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 1 - Renames each readme.md to index.md
|
||||
// e.g.
|
||||
// before => roadmaps/frontend/content/internet/readme.md
|
||||
// after => roadmaps/frontend/content/internet/index.md
|
||||
//
|
||||
// 2 - Replaces the resource tags with short codes
|
||||
// e.g.
|
||||
// <ResourceGroupTitle>Free Content</ResourceGroupTitle>
|
||||
// <BadgeLink colorScheme='yellow' badgeText='Read' href='https://www.w3schools.com/css/'>W3Schools — Learn CSS</BadgeLink>
|
||||
//
|
||||
// {% resources %}
|
||||
// {% Blog "https://www.w3schools.com/css/", "W3Schools — Learn CSS" %}
|
||||
// {% endresources %}
|
||||
//
|
||||
// 3 - Removes the index.md file from within the content dir i.e. to avoid `/frontend` permalink for `/frontend/index.md`
|
||||
// Because we have the `/frontend` permalink serving the actual roadmap and not any content
|
||||
const roadmapsDir = path.join(__dirname, '../src/roadmaps');
|
||||
const roadmapDirs = fs.readdirSync(roadmapsDir);
|
||||
|
||||
roadmapDirs.forEach((roadmapDirName) => {
|
||||
const roadmapDirPath = path.join(roadmapsDir, roadmapDirName);
|
||||
const contentDirPath = path.join(roadmapDirPath, 'content');
|
||||
|
||||
console.log(`[Start] == Migrating ${roadmapDirName}`);
|
||||
|
||||
if (!fs.existsSync(contentDirPath)) {
|
||||
console.log(`Content dir not found ${roadmapDirName}/content`);
|
||||
return;
|
||||
}
|
||||
|
||||
function handleContentDir(parentDirPath) {
|
||||
const dirChildrenNames = fs.readdirSync(parentDirPath);
|
||||
|
||||
dirChildrenNames.forEach((dirChildName) => {
|
||||
let dirChildPath = path.join(parentDirPath, dirChildName);
|
||||
|
||||
// If directory, handle the children for it
|
||||
if (fs.lstatSync(dirChildPath).isDirectory()) {
|
||||
handleContentDir(dirChildPath);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// 1 - Rename directories to remove the numbers
|
||||
//////////////////////////////////////////////////////////
|
||||
// let newDirChildPath = path.join(
|
||||
// path.dirname(dirChildPath),
|
||||
// path.basename(dirChildPath).replace(/^\d+-/, '')
|
||||
// );
|
||||
// fs.renameSync(dirChildPath, dirChildPath);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// 1 - Rename readme.md to index.md
|
||||
//////////////////////////////////////////////////////////
|
||||
if (dirChildPath.endsWith('readme.md')) {
|
||||
const newFilePath = path.join(path.dirname(dirChildPath), `index.md`);
|
||||
|
||||
fs.renameSync(dirChildPath, newFilePath);
|
||||
dirChildPath = newFilePath;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// 2 - Replace the resource tags with short codes
|
||||
//////////////////////////////////////////////////////////
|
||||
if (fs.lstatSync(dirChildPath).isFile()) {
|
||||
const fileContent = fs.readFileSync(dirChildPath, 'utf-8');
|
||||
|
||||
let resourceLinks = [...fileContent.matchAll(/<BadgeLink.+<\/BadgeLink>/g)].map(([fullMatch]) => {
|
||||
// const resourceType = fullMatch.match(/badgeText=["'](.+?)["']/)[1];
|
||||
const link = fullMatch.match(/href=["'](.+?)["']/)[1];
|
||||
const text = fullMatch.match(/>([^<]+)<\/BadgeLink>$/)[1];
|
||||
|
||||
return `- [${text.replaceAll(/['"]/g, '')}](${link})`;
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Replace the dedicated roadmap tag with the short code
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// prettier-ignore
|
||||
const dedicatedRegex = /<DedicatedRoadmap\s*href=['"](.+?)['"]\s*title=['"](.+?)['"]\s*description=['"].+?['"]\s*\/>/;
|
||||
const dedicatedMatches = fileContent.match(dedicatedRegex);
|
||||
|
||||
if (dedicatedMatches) {
|
||||
const [, href, title] = dedicatedMatches;
|
||||
|
||||
resourceLinks = [`- [Visit Dedicated ${title}](${href})`, ...resourceLinks];
|
||||
}
|
||||
|
||||
resourceLinks = ['Visit the following resources to learn more:\n', ...resourceLinks];
|
||||
resourceLinks = resourceLinks.join('\n');
|
||||
|
||||
let newFileContent = fileContent.replace(
|
||||
/<ResourceGroupTitle>([^<\/BadgeLink>]|\S|\s)+<\/BadgeLink>/,
|
||||
resourceLinks
|
||||
);
|
||||
|
||||
// In case if the resources were not wrapped in <ResourceGroupTitle>
|
||||
newFileContent = newFileContent.replace(
|
||||
/<BadgeLink([^<\/BadgeLink>]|\S|\s)+<\/BadgeLink>/,
|
||||
resourceLinks
|
||||
);
|
||||
|
||||
fs.writeFileSync(dirChildPath, newFileContent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleContentDir(contentDirPath);
|
||||
|
||||
// 3 - Removes the index.md file from within the content dir i.e. to avoid `/frontend` permalink for `/frontend/index.md`
|
||||
// Because we have the `/frontend` permalink serving the actual roadmap and not any content
|
||||
const contentRootFile = path.join(contentDirPath, '/index.md');
|
||||
if (fs.existsSync(contentRootFile)) {
|
||||
fs.rmSync(contentRootFile);
|
||||
}
|
||||
|
||||
console.log(` == Migrated ${roadmapDirName}`);
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('json-to-pretty-yaml');
|
||||
|
||||
const contentDirPath = path.join(__dirname, './developer-roadmap/content');
|
||||
const guides = require('./developer-roadmap/content/guides.json');
|
||||
const authors = require('./developer-roadmap/content/authors.json');
|
||||
|
||||
const guideImagesDirPath = path.join(__dirname, './developer-roadmap/public/guides');
|
||||
const newGuideImagesDirPath = path.join(__dirname, '../public/guides');
|
||||
|
||||
// Remove the guide images directory
|
||||
if (fs.existsSync(newGuideImagesDirPath)) {
|
||||
fs.rmSync(newGuideImagesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.cpSync(guideImagesDirPath, newGuideImagesDirPath, { recursive: true });
|
||||
|
||||
// Remove the old guides directory
|
||||
const newGuidesDirPath = path.join(__dirname, '../src/guides');
|
||||
if (fs.existsSync(newGuidesDirPath)) {
|
||||
fs.rmSync(newGuidesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newGuidesDirPath);
|
||||
|
||||
guides.forEach((guide) => {
|
||||
const { id: guideId } = guide;
|
||||
|
||||
const originalGuidePath = path.join(contentDirPath, 'guides', `${guideId}.md`);
|
||||
const newGuidePath = path.join(__dirname, `../src/guides/${guideId}.md`);
|
||||
|
||||
const guideWithoutFrontmatter = fs.readFileSync(originalGuidePath, 'utf8');
|
||||
fs.copyFileSync(originalGuidePath, newGuidePath);
|
||||
|
||||
const guideAuthor = authors.find((author) => author.username === guide.authorUsername);
|
||||
|
||||
const guideFrontMatter = yaml
|
||||
.stringify({
|
||||
title: guide.title,
|
||||
description: guide.description,
|
||||
author: {
|
||||
name: guideAuthor.name,
|
||||
url: `https://twitter.com/${guideAuthor.twitter}`,
|
||||
imageUrl: `${guideAuthor.picture}`,
|
||||
},
|
||||
seo: {
|
||||
title: `${guide.title} - roadmap.sh`,
|
||||
description: guide.description,
|
||||
},
|
||||
isNew: guide.isNew,
|
||||
type: guide.type,
|
||||
date: guide.createdAt.replace(/T.*/, ''),
|
||||
sitemap: {
|
||||
priority: 0.7,
|
||||
changefreq: 'weekly',
|
||||
},
|
||||
tags: ['guide', `${guide.type}-guide`, `guide-sitemap`],
|
||||
})
|
||||
.replace(/date: "(.+?)"/, 'date: $1');
|
||||
|
||||
const guideWithUpdatedUrls = guideWithoutFrontmatter
|
||||
.replace(/\[\!\[\]\((.+?\.png)\)\]\((.+?\.png)\)/g, '[]($2)')
|
||||
.replace(/\[\!\[\]\((.+?\.svg)\)\]\((.+?\.svg)\)/g, '[]($2)')
|
||||
.replace(/\/http/g, 'http')
|
||||
.replace(/]\(\/guides\/(.+?)\.png\)/g, '](/guides/$1.png)')
|
||||
.replace(/<iframe/g, '<iframe class="w-full aspect-video mb-5"')
|
||||
.replace(/<iframe(.+?)\s?\/>/g, '<iframe$1></iframe>');
|
||||
|
||||
const guideWithFrontmatter = `---\n${guideFrontMatter}---\n\n${guideWithUpdatedUrls}`;
|
||||
|
||||
console.log(`Writing guide ${guideId} to disk`);
|
||||
fs.writeFileSync(newGuidePath, guideWithFrontmatter);
|
||||
});
|
||||
|
||||
const oldAuthorAssetsPath = path.join(__dirname, 'developer-roadmap/public/authors');
|
||||
const newAuthorAssetsPath = path.join(__dirname, '../public/authors');
|
||||
|
||||
if (fs.existsSync(newAuthorAssetsPath)) {
|
||||
fs.rmSync(newAuthorAssetsPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.cpSync(oldAuthorAssetsPath, newAuthorAssetsPath, { recursive: true });
|
||||
28
bin/readme.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## CLI Tools
|
||||
> A bunch of CLI scripts to make the development easier
|
||||
|
||||
## `roadmap-links.cjs`
|
||||
|
||||
Generates a list of all the resources links in any roadmap file.
|
||||
|
||||
## `compress-jsons.cjs`
|
||||
|
||||
Compresses all the JSON files in the `public/jsons` folder
|
||||
|
||||
## `roadmap-content.cjs`
|
||||
|
||||
This command is used to create the content folders and files for the interactivity of the roadmap. You can use the below command to generate the roadmap skeletons inside a roadmap directory:
|
||||
|
||||
```bash
|
||||
npm run roadmap-content [frontend|backend|devops|...]
|
||||
```
|
||||
|
||||
For the content skeleton to be generated, we should have proper grouping, and the group names in the project files. You can follow the steps listed below in order to add the meta information to the roadmap.
|
||||
|
||||
- Remove all the groups from the roadmaps through the project editor. Select all and press `cmd+shift+g`
|
||||
- Identify the boxes that should be clickable and group them together with `cmd+shift+g`
|
||||
- Assign the name to the groups.
|
||||
- Group names have the format of `[sort]-[slug]` e.g. `100-internet`. Each group name should start with a number starting from 100 which helps with sorting of the directories and the files. Groups at the same level have the sequential sorting information.
|
||||
- Each groups children have a separate group and have the name similar to `[sort]-[parent-slug]:[child-slug]` where sort refers to the sorting of the `child-slug` and not the parent. Also parent-slug does not need to have the sorting information as a part of slug e.g. if parent was `100-internet` the children would be `100-internet:how-does-the-internet-work`, `101-internet:what-is-http`, `102-internet:browsers`.
|
||||
|
||||
|
||||
163
bin/roadmap-content.cjs
Normal file
@@ -0,0 +1,163 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the roadmaps
|
||||
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the roadmap content files
|
||||
const roadmapDirName = fs
|
||||
.readdirSync(ROADMAP_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === roadmapId);
|
||||
|
||||
if (!roadmapDirName) {
|
||||
console.error('Roadmap directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDirPath = path.join(ROADMAP_CONTENT_DIR, roadmapDirName);
|
||||
const roadmapContentDirPath = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If roadmap content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(roadmapContentDirPath)) {
|
||||
console.error(`Roadmap content already exists @ ${roadmapContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
// e.g. 104
|
||||
const sortOrder = controlName.match(/^\d+/)?.[0];
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || !sortOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. testing-your-apps:other-options
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlNameWithoutSortOrder.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
dirSortOrders[controlNameWithoutSortOrder] = Number(sortOrder);
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree, dirSortOrders);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree, dirSortOrders };
|
||||
}
|
||||
|
||||
const roadmap = require(path.join(__dirname, `../public/jsons/roadmaps/${roadmapId}`));
|
||||
const controls = roadmap.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating and also calculate the sort orders
|
||||
const dirTree = {};
|
||||
const dirSortOrders = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree, dirSortOrders);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param sortOrders Mapping from groupName to sort order
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, sortOrders, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(roadmapContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replace(/(^\d+?-)/g, '') // Remove sorting information
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
const sortOrder = sortOrders[groupName] || '';
|
||||
|
||||
// Attach sorting information to dirname
|
||||
// e.g. /roadmaps/100-frontend/content/internet
|
||||
// ———> /roadmaps/100-frontend/content/103-internet
|
||||
if (sortOrder) {
|
||||
parentDir = parentDir.replace(/(.+?)([^\/]+)?$/, `$1${sortOrder}-$2`);
|
||||
}
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
dirSortOrders,
|
||||
filePaths
|
||||
);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(roadmapContentDirPath, dirTree, dirSortOrders);
|
||||
console.log('Created roadmap content directory structure');
|
||||
44
bin/roadmap-links.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const roadmapId = process.argv[2];
|
||||
if (!roadmapId) {
|
||||
console.error('Error: roadmapId is required');
|
||||
}
|
||||
|
||||
const fullPath = path.join(__dirname, `../src/roadmaps/${roadmapId}`);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Error: path not found: ${fullPath}!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function readFiles(folderPath) {
|
||||
const stats = fs.lstatSync(folderPath);
|
||||
|
||||
if (stats.isFile()) {
|
||||
return [folderPath];
|
||||
}
|
||||
|
||||
const folderContent = fs.readdirSync(folderPath);
|
||||
let files = [];
|
||||
|
||||
for (const file of folderContent) {
|
||||
const filePath = path.join(folderPath, file);
|
||||
|
||||
files = [...files, ...readFiles(filePath)];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const files = readFiles(fullPath);
|
||||
let allLinks = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileContent = fs.readFileSync(file, 'utf-8');
|
||||
const matches = [...fileContent.matchAll(/\[[^\]]+]\((https?:\/\/[^)]+)\)/g)];
|
||||
|
||||
allLinks = [...allLinks, ...matches.map((match) => match[1])];
|
||||
});
|
||||
|
||||
allLinks.map((link) => console.log(link));
|
||||
@@ -1,116 +0,0 @@
|
||||
module.exports = {
|
||||
angular: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2277.8,
|
||||
},
|
||||
},
|
||||
'aspnet-core': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2773.45,
|
||||
},
|
||||
},
|
||||
backend: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2840.4,
|
||||
},
|
||||
},
|
||||
blockchain: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2173.87,
|
||||
},
|
||||
},
|
||||
'computer-science': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 3009.05,
|
||||
},
|
||||
},
|
||||
'design-system': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2309.7,
|
||||
},
|
||||
},
|
||||
devops: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2527.46,
|
||||
},
|
||||
},
|
||||
flutter: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2042.2,
|
||||
},
|
||||
},
|
||||
frontend: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2734.48,
|
||||
},
|
||||
},
|
||||
golang: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1495.21,
|
||||
},
|
||||
},
|
||||
java: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1167.29,
|
||||
},
|
||||
},
|
||||
javascript: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2438.9,
|
||||
},
|
||||
},
|
||||
nodejs: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2474.06,
|
||||
},
|
||||
},
|
||||
python: {
|
||||
dimensions: {
|
||||
width: 992,
|
||||
height: 1259.03,
|
||||
},
|
||||
},
|
||||
qa: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 2107.75,
|
||||
},
|
||||
},
|
||||
react: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1570.26,
|
||||
},
|
||||
},
|
||||
'software-architect': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1882.18,
|
||||
},
|
||||
},
|
||||
'software-design-architecture': {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1764.66,
|
||||
},
|
||||
},
|
||||
vue: {
|
||||
dimensions: {
|
||||
width: 968,
|
||||
height: 1657.07,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('json-to-pretty-yaml');
|
||||
const roadmapMetas = require('./roadmap-metas.cjs');
|
||||
|
||||
const oldAssetsPath = path.join(__dirname, 'developer-roadmap/public');
|
||||
const newAssetsPath = path.join(__dirname, '../public/');
|
||||
|
||||
// Create JSONs dir
|
||||
const newJsonsPath = path.join(newAssetsPath, 'jsons');
|
||||
if (fs.existsSync(newJsonsPath)) {
|
||||
fs.rmSync(newJsonsPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newJsonsPath);
|
||||
|
||||
// Create PDFs dir
|
||||
const newPdfsPath = path.join(newAssetsPath, 'pdfs');
|
||||
if (fs.existsSync(newPdfsPath)) {
|
||||
fs.rmSync(newPdfsPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newPdfsPath);
|
||||
|
||||
const oldRoadmapsDirPath = path.join(__dirname, 'developer-roadmap/content/roadmaps');
|
||||
const newRoadmapsDirPath = path.join(__dirname, '../src/roadmaps');
|
||||
|
||||
if (fs.existsSync(newRoadmapsDirPath)) {
|
||||
fs.rmSync(newRoadmapsDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newRoadmapsDirPath);
|
||||
|
||||
const oldRoadmaps = fs
|
||||
.readdirSync(oldRoadmapsDirPath)
|
||||
.map((roadmapDirName) => path.join(oldRoadmapsDirPath, roadmapDirName));
|
||||
|
||||
const orderInfo = {};
|
||||
const typeCounter = {
|
||||
role: 1,
|
||||
tool: 1,
|
||||
};
|
||||
|
||||
// Calculate the sorting information for the roadmaps
|
||||
oldRoadmaps.forEach((oldRoadmapPath) => {
|
||||
const roadmapId = path.basename(oldRoadmapPath).replace(/\d+-/g, '').toLowerCase();
|
||||
const oldRoadmapMeta = require(path.join(oldRoadmapPath, 'meta.json'));
|
||||
|
||||
orderInfo[roadmapId] = typeCounter[oldRoadmapMeta.type];
|
||||
typeCounter[oldRoadmapMeta.type] += 1;
|
||||
});
|
||||
|
||||
// Iterate and create new roadmaps
|
||||
oldRoadmaps.forEach((oldRoadmapPath) => {
|
||||
const roadmapId = path.basename(oldRoadmapPath).replace(/\d+-/g, '').toLowerCase();
|
||||
|
||||
const metaToMerge = roadmapMetas[roadmapId] ?? {};
|
||||
const oldRoadmapMeta = require(path.join(oldRoadmapPath, 'meta.json'));
|
||||
const isTextual = oldRoadmapMeta?.landingPath?.endsWith('.md');
|
||||
|
||||
const hasContentDir = fs.existsSync(path.join(oldRoadmapPath, 'content'));
|
||||
|
||||
const roadmapFileContent = isTextual
|
||||
? fs.readFileSync(path.join(oldRoadmapPath, oldRoadmapMeta.landingPath), 'utf8')
|
||||
: '';
|
||||
|
||||
const roadmapFileContentWithUpdatedUrls = roadmapFileContent
|
||||
.replace(/\[\!\[\]\((.+?\.png)\)\]\((.+?\.png)\)/g, '[](/assets$2)')
|
||||
.replace(/\[\!\[\]\((.+?\.svg)\)\]\((.+?\.svg)\)/g, '[](/assets$2)')
|
||||
.replace(/\[\!\[\]\((.+?\.svg)\)\]\((.+?\.png)\)/g, '[](/assets$2)')
|
||||
.replace(/assetshttp\//g, 'http')
|
||||
.replace(/assetshttps:\/\//g, 'https://')
|
||||
.replace(/\/http/g, 'http')
|
||||
.replace(/]\(\/roadmaps\/(.+?)\.png\)/g, '](/assets/roadmaps/$1.png)')
|
||||
.replace(/]\(\/roadmaps\/(.+?)\.svg\)/g, '](/assets/roadmaps/$1.svg)')
|
||||
.replace(/<iframe/g, '<iframe class="w-full aspect-video mb-5"')
|
||||
.replace(/<iframe(.+?)\s?\/>/g, '<iframe$1></iframe>');
|
||||
|
||||
const hasJson = fs.existsSync(path.join(oldAssetsPath, `/project/${roadmapId}.json`));
|
||||
|
||||
const newRoadmapMeta = {
|
||||
...( hasJson ? { jsonUrl: `/jsons/${roadmapId}.json`} : {}),
|
||||
pdfUrl: `/pdfs/${roadmapId}.pdf`,
|
||||
order: orderInfo[roadmapId],
|
||||
featuredTitle:
|
||||
oldRoadmapMeta.featuredTitle === 'Software Design and Architecture'
|
||||
? 'Software Design'
|
||||
: oldRoadmapMeta.featuredTitle,
|
||||
featuredDescription: oldRoadmapMeta.featuredDescription,
|
||||
title: oldRoadmapMeta.title,
|
||||
description: oldRoadmapMeta.description,
|
||||
isNew: oldRoadmapMeta.isNew,
|
||||
hasTopics: hasContentDir,
|
||||
...metaToMerge,
|
||||
seo: oldRoadmapMeta.seo,
|
||||
relatedRoadmaps: oldRoadmapMeta.relatedRoadmaps,
|
||||
sitemap: {
|
||||
priority: 1,
|
||||
changefreq: 'monthly',
|
||||
},
|
||||
tags: ['roadmap', 'main-sitemap', `${oldRoadmapMeta.type === 'tool' ? 'skill' : oldRoadmapMeta.type}-roadmap`],
|
||||
};
|
||||
|
||||
const frontmatter = yaml.stringify(newRoadmapMeta);
|
||||
const newRoadmapDirPath = path.join(newRoadmapsDirPath, roadmapId);
|
||||
const newRoadmapFilePath = path.join(newRoadmapDirPath, `/${roadmapId}.md`);
|
||||
|
||||
fs.mkdirSync(newRoadmapDirPath);
|
||||
fs.writeFileSync(newRoadmapFilePath, `---\n${frontmatter}---\n\n${roadmapFileContentWithUpdatedUrls}`);
|
||||
|
||||
const jsonFile = path.join(oldAssetsPath, oldRoadmapMeta.jsonUrl || '/unknown');
|
||||
const pdfFile = path.join(oldAssetsPath, oldRoadmapMeta.pdfUrl || '/unknown');
|
||||
|
||||
if (fs.existsSync(jsonFile)) {
|
||||
fs.copyFileSync(jsonFile, path.join(newJsonsPath, `${roadmapId}.json`));
|
||||
}
|
||||
|
||||
if (fs.existsSync(pdfFile)) {
|
||||
fs.copyFileSync(pdfFile, path.join(newPdfsPath, `${roadmapId}.pdf`));
|
||||
}
|
||||
|
||||
// Copy the content directory
|
||||
const oldRoadmapContentDir = path.join(oldRoadmapPath, 'content');
|
||||
if (fs.existsSync(oldRoadmapContentDir)) {
|
||||
fs.cpSync(oldRoadmapContentDir, path.join(newRoadmapDirPath, 'content'), { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
const roadmapAssets = path.join(oldAssetsPath, 'roadmaps');
|
||||
if (fs.existsSync(roadmapAssets)) {
|
||||
fs.cpSync(roadmapAssets, path.join(newAssetsPath, 'roadmaps'), { recursive: true });
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Change working directory to the directory of this script
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -d "./developer-roadmap" ]; then
|
||||
git clone --depth 1 -b master git@github.com:kamranahmedse/developer-roadmap.git
|
||||
fi
|
||||
|
||||
echo "Removing old directories"
|
||||
rm -rf ../src/videos
|
||||
rm -rf ../src/guides
|
||||
rm -rf ../src/roadmaps
|
||||
|
||||
rm -rf ../public/jsons
|
||||
rm -rf ../public/pdfs
|
||||
|
||||
echo "=== Migrating Roadmaps ==="
|
||||
node roadmap-migrator.cjs
|
||||
|
||||
echo "=== Migrating Content ==="
|
||||
node content-migrator.cjs
|
||||
|
||||
echo "=== Migrating Guides ==="
|
||||
node guide-migrator.cjs
|
||||
|
||||
echo "=== Migrating Videos ==="
|
||||
node video-migrator.cjs
|
||||
@@ -1,58 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('json-to-pretty-yaml');
|
||||
|
||||
const contentDirPath = path.join(__dirname, './developer-roadmap/content');
|
||||
const videos = require('./developer-roadmap/content/videos.json');
|
||||
|
||||
// Remove the old videos directory
|
||||
const newVideosDirPath = path.join(__dirname, '../src/videos');
|
||||
if (fs.existsSync(newVideosDirPath)) {
|
||||
fs.rmSync(newVideosDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(newVideosDirPath);
|
||||
|
||||
videos.forEach((video) => {
|
||||
const { id: videoId } = video;
|
||||
|
||||
const originalVideoPath = path.join(
|
||||
contentDirPath,
|
||||
'videos',
|
||||
`${videoId}.md`
|
||||
);
|
||||
|
||||
const newVideoPath = path.join(__dirname, `../src/videos/${videoId}.md`);
|
||||
|
||||
const videoWithoutFrontmatter = fs.readFileSync(originalVideoPath, 'utf8');
|
||||
fs.copyFileSync(originalVideoPath, newVideoPath);
|
||||
|
||||
const videoFrontMatter = yaml
|
||||
.stringify({
|
||||
title: video.title,
|
||||
description: video.description,
|
||||
duration: video.duration,
|
||||
isNew: video.isNew,
|
||||
date: video.createdAt.replace(/T.*/, ''),
|
||||
author: {
|
||||
name: 'Kamran Ahmed',
|
||||
url: `https://twitter.com/kamranahmedse`,
|
||||
imageUrl: `/authors/kamranahmedse.jpeg`,
|
||||
},
|
||||
sitemap: {
|
||||
priority: 0.7,
|
||||
changefreq: 'weekly',
|
||||
},
|
||||
tags: ['video', `video-sitemap`],
|
||||
})
|
||||
.replace(/date: "(.+?)"/, 'date: $1');
|
||||
|
||||
const videoWithIframeClass = videoWithoutFrontmatter
|
||||
.replace(/<iframe/g, '<iframe class="w-full aspect-video mb-5"')
|
||||
.replace(/<iframe(.+?)\s?\/>/g, '<iframe$1></iframe>');
|
||||
|
||||
const videoWithFrontmatter = `---\n${videoFrontMatter}---\n\n${videoWithIframeClass}`;
|
||||
|
||||
console.log(`Writing video ${videoId} to disk`);
|
||||
fs.writeFileSync(newVideoPath, videoWithFrontmatter);
|
||||
});
|
||||
@@ -3,10 +3,10 @@
|
||||
First of all thank you for considering to contribute. Please look at the details below:
|
||||
|
||||
- [Contribution](#contribution)
|
||||
- [New Roadmaps](#new-roadmaps)
|
||||
- [Existing Roadmaps](#existing-roadmaps)
|
||||
- [Adding Content](#adding-content)
|
||||
- [Guidelines](#guidelines)
|
||||
- [New Roadmaps](#new-roadmaps)
|
||||
- [Existing Roadmaps](#existing-roadmaps)
|
||||
- [Adding Content](#adding-content)
|
||||
- [Guidelines](#guidelines)
|
||||
|
||||
## New Roadmaps
|
||||
|
||||
@@ -23,7 +23,7 @@ For the existing roadmaps, please follow the details listed for the nature of co
|
||||
|
||||
## Adding Content
|
||||
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/roadmap-astro/tree/master/src/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
|
||||
- Content must be in English.
|
||||
- Put a brief description about the topic on top of the file and the a list of links below with each link having title of the URL.
|
||||
|
||||
26
package.json
@@ -10,25 +10,31 @@
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d dist -t",
|
||||
"sync-content": "sh ./bin/sync-content.sh",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs"
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-content": "node bin/best-practice-content.cjs",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.0.0",
|
||||
"@astrojs/tailwind": "^2.1.3",
|
||||
"astro": "^1.8.0",
|
||||
"astro-compress": "^1.1.24",
|
||||
"astro-critters": "^1.1.24",
|
||||
"astro": "^1.9.2",
|
||||
"astro-compress": "^1.1.28",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.1",
|
||||
"roadmap-renderer": "^1.0.4",
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"gh-pages": "^4.0.0",
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"gh-pages": "^5.0.0",
|
||||
"json-to-pretty-yaml": "^1.2.2",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-plugin-astro": "^0.7.0"
|
||||
"markdown-it": "^13.0.1",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-astro": "^0.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
108
playwright.config.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
import { devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: {
|
||||
// ...devices['Desktop Firefox'],
|
||||
// },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: {
|
||||
// ...devices['Desktop Safari'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1614
pnpm-lock.yaml
generated
BIN
public/best-practices/frontend-performance.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 835 KiB |
BIN
public/images/ambassador-img.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/images/devops-ebook.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/images/system-design.png
Normal file
|
After Width: | Height: | Size: 832 KiB |
1
public/jsons/best-practices/frontend-performance.json
Normal file
1
public/jsons/roadmaps/blockchain.json
Normal file
1
public/jsons/roadmaps/software-design-architecture.json
Normal file
1
public/jsons/roadmaps/spring-boot.json
Normal file
1
public/jsons/roadmaps/system-design.json
Normal file
1
public/jsons/roadmaps/typescript.json
Normal file
BIN
public/pdfs/best-practices/frontend-performance.pdf
Normal file
BIN
public/pdfs/roadmaps/software-architect.pdf
Normal file
BIN
public/pdfs/roadmaps/spring-boot.pdf
Normal file
BIN
public/pdfs/roadmaps/system-design.pdf
Normal file
BIN
public/pdfs/roadmaps/typescript.pdf
Normal file
BIN
public/roadmaps/typescript.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="public/brand.png" height="128">
|
||||
<img src="public/images/brand.png" height="128">
|
||||
<h2 align="center"><a href="https://roadmap.sh">roadmap.sh</a></h2>
|
||||
<p align="center">Community driven roadmaps, articles and resources for developers<p>
|
||||
<p align="center">
|
||||
@@ -48,10 +48,12 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Python Roadmap](https://roadmap.sh/python)
|
||||
- [Go Roadmap](https://roadmap.sh/golang)
|
||||
- [Java Roadmap](https://roadmap.sh/java)
|
||||
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
|
||||
- [Design System Roadmap](https://roadmap.sh/design-system)
|
||||
- [DBA Roadmap](https://roadmap.sh/postgresql-dba)
|
||||
- [Blockchain Roadmap](https://roadmap.sh/blockchain)
|
||||
- [ASP.NET Core Roadmap](https://roadmap.sh/aspnet-core)
|
||||
- [System Design Roadmap](https://roadmap.sh/system-design)
|
||||
|
||||

|
||||
|
||||
33
sitemap.mjs
@@ -5,23 +5,29 @@ async function getRoadmapIds() {
|
||||
return fs.readdir(path.join(process.cwd(), 'src/roadmaps'));
|
||||
}
|
||||
|
||||
async function getBestPracticesIds() {
|
||||
return fs.readdir(path.join(process.cwd(), 'src/best-practices'));
|
||||
}
|
||||
|
||||
export function shouldIndexPage(page) {
|
||||
return ![
|
||||
'https://roadmap.sh/404/',
|
||||
'https://roadmap.sh/terms/',
|
||||
'https://roadmap.sh/privacy/',
|
||||
'https://roadmap.sh/pdfs/',
|
||||
'https://roadmap.sh/404',
|
||||
'https://roadmap.sh/terms',
|
||||
'https://roadmap.sh/privacy',
|
||||
'https://roadmap.sh/pdfs',
|
||||
].includes(page);
|
||||
}
|
||||
|
||||
export async function serializeSitemap(item) {
|
||||
const highPriorityPages = [
|
||||
'https://roadmap.sh/',
|
||||
'https://roadmap.sh/about/',
|
||||
'https://roadmap.sh/roadmaps/',
|
||||
'https://roadmap.sh/guides/',
|
||||
'https://roadmap.sh/videos/',
|
||||
...(await getRoadmapIds()).map((id) => `https://roadmap.sh/${id}/`),
|
||||
'https://roadmap.sh',
|
||||
'https://roadmap.sh/about',
|
||||
'https://roadmap.sh/roadmaps',
|
||||
'https://roadmap.sh/best-practices',
|
||||
'https://roadmap.sh/guides',
|
||||
'https://roadmap.sh/videos',
|
||||
...(await getRoadmapIds()).flatMap((id) => [`https://roadmap.sh/${id}`, `https://roadmap.sh/${id}/topics`]),
|
||||
...(await getBestPracticesIds()).map((id) => `https://roadmap.sh/best-practices/${id}`),
|
||||
];
|
||||
|
||||
// Roadmaps and other high priority pages
|
||||
@@ -32,22 +38,17 @@ export async function serializeSitemap(item) {
|
||||
// @ts-ignore
|
||||
changefreq: 'monthly',
|
||||
priority: 1,
|
||||
lastmod: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Guide and video pages
|
||||
if (
|
||||
item.url.startsWith('https://roadmap.sh/guides/') ||
|
||||
item.url.startsWith('https://roadmap.sh/videos/')
|
||||
) {
|
||||
if (item.url.startsWith('https://roadmap.sh/guides') || item.url.startsWith('https://roadmap.sh/videos')) {
|
||||
return {
|
||||
...item,
|
||||
// @ts-ignore
|
||||
changefreq: 'monthly',
|
||||
priority: 0.9,
|
||||
lastmod: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Stylesheet Complexity
|
||||
|
||||
> Analyzing your stylesheets can help you to flag issues, redundancies and duplicate CSS selectors.
|
||||
|
||||
Sometimes you may have redundancies or validation errors in your CSS, analysing your CSS files and removed these complexities can help you to speed up your CSS files (because your browser will read them faster).
|
||||
|
||||
Your CSS should be organized, using a CSS preprocessor can help you with that. Some online tools listed below can also help you analysing and correct your code.
|
||||
|
||||
- [TestMyCSS | Optimize and Check CSS Performance](http://www.testmycss.com/)
|
||||
- [CSS Stats](https://cssstats.com/)
|
||||
- [macbre/analyze-css: CSS selectors complexity and performance analyzer](https://github.com/macbre/analyze-css)
|
||||
- [Project Wallace](https://www.projectwallace.com/) is like CSS Stats but stores stats over time so you can track your changes
|
||||
@@ -0,0 +1,13 @@
|
||||
# JavaScript Performance
|
||||
|
||||
> Check for performance problems in your JavaScript files (and CSS too)
|
||||
|
||||
JavaScript complexity can slow down runtime performance. Identifying these possible issues are essential to offer the smoothest user experience.
|
||||
|
||||
Use the Timeline tool in the Chrome Developer Tool to evaluate scripts events and found the one that may take too much time.
|
||||
|
||||
- [Speed Up JavaScript Execution | Tools for Web Developers](https://developers.google.com/web/tools/chrome-devtools/rendering-tools/js-execution)
|
||||
- [JavaScript Profiling With The Chrome Developer Tools](https://www.smashingmagazine.com/2012/06/javascript-profiling-chrome-developer-tools/)
|
||||
- [How to Record Heap Snapshots | Tools for Web Developers](https://developers.google.com/web/tools/chrome-devtools/memory-problems/heap-snapshots)
|
||||
- [Chapter 22 - Profiling the Frontend - Blackfire](https://blackfire.io/docs/book/22-frontend-profiling)
|
||||
- [30 Tips To Improve Javascript Performance](http://www.monitis.com/blog/30-tips-to-improve-javascript-performance/)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Serve Reachable Files
|
||||
|
||||
> Avoid requesting unreachable files (404)
|
||||
|
||||
404 request can slow down the performance of your website and negatively impact the user experience. Additionally, they can also cause search engines to crawl and index non-existent pages, which can negatively impact your search engine rankings. To avoid 404 requests, ensure that all links on your website are valid and that any broken links are fixed promptly.
|
||||
|
||||
- [How to avoid Bad Requests?](https://varvy.com/pagespeed/avoid-bad-requests.html)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Avoid Base64 Images
|
||||
|
||||
> You could eventually convert tiny images to base64 but it's actually not the best practice.
|
||||
|
||||
Using Base64 encoded images in your frontend can have several drawbacks.
|
||||
|
||||
First, Base64 encoded images are larger in size than their binary counterparts, which can slow down the loading time of your website.
|
||||
|
||||
Second, because Base64 encoded images are embedded directly in the HTML or CSS, they are included in the initial page load, which can cause delays for users with slower internet connections.
|
||||
|
||||
Third, Base64 encoded images do not benefit from browser caching, as they are part of the HTML or CSS and not a separate resource. So, every time the page is loaded, the images will be re-downloaded, even if the user has visited the page before.
|
||||
|
||||
Fourth, Base64 encoded images are not compatible with some old browser versions.
|
||||
|
||||
Instead of using Base64 encoded images, it is generally recommended to use binary image files and reference them using an img tag in HTML, with a standard src attribute. This allows the browser to cache the image and use it for subsequent page loads, resulting in faster loading times and better user experience.
|
||||
|
||||
- [Base64 Encoding & Performance, Part 1 and 2 by Harry Roberts](https://csswizardry.com/2017/02/base64-encoding-and-performance/)
|
||||
- [A closer look at Base64 image performance – The Page Not Found Blog](http://www.andygup.net/a-closer-look-at-base64-image-performance/)
|
||||
- [When to base64 encode images (and when not to) | David Calhoun](https://www.davidbcalhoun.com/2011/when-to-base64-encode-images-and-when-not-to/)
|
||||
- [Base64 encoding images for faster pages | Performance and seo factors](https://varvy.com/pagespeed/base64-images.html)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Avoid Inline CSS
|
||||
|
||||
> Avoid using embed or inline CSS inside your `<body>` (Not valid for HTTP/2)
|
||||
|
||||
One of the first reason it's because it's a good practice to separate content from design. It also helps you have a more maintainable code and keep your site accessible. But regarding performance, it's simply because it decreases the file-size of your HTML pages and the load time.
|
||||
|
||||
Always use external stylesheets or embed CSS in your `<head>` (and follow the others CSS performance rules)
|
||||
|
||||
- [Observe CSS Best Practices: Avoid CSS Inline Styles](https://www.lifewire.com/avoid-inline-styles-for-css-3466846)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Avoid Inline JavaScript
|
||||
|
||||
> Avoid having multiple JavaScript codes embedded in the middle of your body. Regroup your JavaScript code inside external files or eventually in the `<head>` or at the end of your page (before `</body>`).
|
||||
|
||||
Placing JavaScript embedded code directly in your `<body>` can slow down your page because it loads while the DOM is being built. The best option is to use external files with async or defer to avoid blocking the DOM. Another option is to place some scripts inside your `<head>`. Most of the time analytics code or small script that need to load before the DOM gets to main processing.
|
||||
|
||||
Ensure that all your files are loaded using `async` or `defer` and decide wisely the code that you will need to inject in your `<head>`.
|
||||
|
||||
- [11 Tips to Optimize JavaScript and Improve Website Loading Speeds](https://www.upwork.com/hiring/development/11-tips-to-optimize-javascript-and-improve-website-loading-speeds/)
|
||||
@@ -0,0 +1,5 @@
|
||||
# Bundlephobia
|
||||
|
||||
Bundlephobia is a website and npm package that allows developers to search for the size of any JavaScript package before adding it to their project. It provides information on the size of the package, as well as the size of its dependencies, to help developers make informed decisions about which packages to use and how they may impact the overall size of their application. Bundlephobia also offers badges which can be added to your GitHub README.md file to show the size of your package.
|
||||
|
||||
- [Official Website - Bundlephobia](https://bundlephobia.com/)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Check Dependency Size
|
||||
|
||||
> Ensure to use wisely external libraries, most of the time, you can use a lighter library for a same functionality.
|
||||
|
||||
You may be tempted to use one of the 745 000 packages you can find on npm, but you need to choose the best package for your needs. For example, MomentJS is an awesome library but with a lot of methods you may never use, that's why Day.js was created. It's just 2kB vs 16.4kB gz for Moment.
|
||||
|
||||
Always compare and choose the best and lighter library for your needs. You can also use tools like [npm trends](http://www.npmtrends.com/) to compare NPM package downloads counts or [Bundlephobia](https://bundlephobia.com/) to know the size of your dependencies.
|
||||
|
||||
- [ai/size-limit: Prevent JS libraries bloat](https://github.com/ai/size-limit)
|
||||
- [webpack-bundle-analyzer - npm](https://www.npmjs.com/package/webpack-bundle-analyzer)
|
||||
- [js-dependency-viewer - npm](https://www.npmjs.com/package/js-dependency-viewer)
|
||||
- [Size Limit: Make the Web lighter](https://evilmartians.com/chronicles/size-limit-make-the-web-lighter)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Image Format
|
||||
|
||||
> Choose your image format appropriately
|
||||
|
||||
To ensure that your images don't slow your website, choose the format that will correspond to your image. If it's a photo, JPEG is most of the time more appropriate than PNG or GIF. But don't forget to look a the nex-gen formats which can reduce the size of your files. Each image format has pros and cons, it's important to know these to make the best choice possible.
|
||||
|
||||
Use [Lighthouse](https://developers.google.com/web/tools/lighthouse/) to identify which images can eventually use next-gen formats (like JPEG 2000m JPEG XR or WebP). Compare different formats, sometimes using PNG8 is better than PNG16, sometimes it's not.
|
||||
|
||||
- [Serve Images in Next-Gen Formats](https://developers.google.com/web/tools/lighthouse/audits/webp)
|
||||
- [What Is the Right Image Format for Your Website?](https://www.sitepoint.com/what-is-the-right-image-format-for-your-website/)
|
||||
- [PNG8 - The Clear Winner](https://www.sitepoint.com/png8-the-clear-winner/)
|
||||
- [8-bit vs 16-bit - What Color Depth You Should Use And Why It Matters](https://www.diyphotography.net/8-bit-vs-16-bit-color-depth-use-matters/)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Chrome DevTools
|
||||
|
||||
Chrome DevTools is a set of web development tools built into the Google Chrome browser. It allows developers to inspect and debug the front-end of web applications.
|
||||
|
||||
Some of the benefits of using Chrome DevTools include:
|
||||
|
||||
- Inspecting and editing HTML and CSS in real-time, which can be useful for identifying and fixing layout issues
|
||||
- Debugging JavaScript code, including setting breakpoints, stepping through code, and examining the call stack
|
||||
- Profiling the performance of web pages, including identifying slow-running JavaScript code and analyzing network requests
|
||||
- Inspecting and debugging browser-side storage, including cookies, local storage, and indexedDB
|
||||
- Auditing web pages for performance, accessibility, and best practices
|
||||
|
||||
Chrome DevTools are a powerful and essential tool for web developers, as it helps to improve debugging, testing, and optimization of the web application.
|
||||
|
||||
- [Chrome DevTools Docs](https://developer.chrome.com/docs/devtools/)
|
||||
@@ -0,0 +1,19 @@
|
||||
# Images Optimization
|
||||
|
||||
> Your images are optimized, compressed without direct impact to the end user.
|
||||
|
||||
Optimized images load faster in your browser and consume less data.
|
||||
|
||||
- Try using CSS3 effects when it's possible (instead of a small image
|
||||
- When it's possible, use fonts instead of text encoded in your images
|
||||
- Use SVG
|
||||
- Use a tool and specify a level compression under 85.
|
||||
|
||||
- [Image Optimization](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/image-optimization)
|
||||
- [Essential Image Optimization](https://images.guide/)
|
||||
- [TinyJPG – Compress JPEG images intelligently](https://tinyjpg.com/)
|
||||
- [Kraken.io - Online Image Optimizer](https://kraken.io/web-interface)
|
||||
- [Compressor.io](https://compressor.io/compress)
|
||||
- [Cloudinary - Image Analysis Tool](https://webspeedtest.cloudinary.com)
|
||||
- [ImageEngine - Image Webpage Loading Test](https://demo.imgeng.in)
|
||||
- [SVGOMG - Optimize SVG vector graphics files](https://jakearchibald.github.io/svgomg/)
|
||||
@@ -0,0 +1,11 @@
|
||||
# CSS Concatenation
|
||||
|
||||
> CSS files are concatenated in a single file (Not always valid for HTTP/2)
|
||||
|
||||
If you are still using HTTP/1, you may need to still concatenate your files, it's less true if your server use HTTP/2 (tests should be made).
|
||||
|
||||
- Use online tool or any plugin before or during your build or your deployment to concatenate your files.
|
||||
- Ensure, of course, that concatenation does not break your project
|
||||
|
||||
- [HTTP: Optimizing Application Delivery - High Performance Browser Networking (O'Reilly)](https://hpbn.co/optimizing-application-delivery/#optimizing-for-http2)
|
||||
- [Performance Best Practices in the HTTP/2 Era](https://deliciousbrains.com/performance-best-practices-http2/)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Cookie Size
|
||||
|
||||
> If you are using cookies, be sure each cookie doesn't exceed 4096 bytes and your domain name doesn't have more than 20 cookies.
|
||||
|
||||
Cookies are exchanged in the HTTP headers between web servers and browsers. It's important to keep the size of cookies as low as possible to minimize the impact on the user's response time.
|
||||
|
||||
- [Cookie specification: RFC 6265](https://tools.ietf.org/html/rfc6265)
|
||||
- [Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)
|
||||
- [Browser Cookie Limits](http://browsercookielimits.squawky.net/)
|
||||
- [Website Performance: Cookies Don't Taste So Good - Monitis Blog](http://www.monitis.com/blog/website-performance-cookies-dont-taste-so-good/)
|
||||
- [Google's Web Performance Best Practices #3: Minimize Request Overhead - GlobalDots Blog](https://www.globaldots.com/googles-web-performance-best-practices-3-minimize-request-overhead/)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Enable Compression
|
||||
|
||||
Use a compression method such as Gzip or Brotli to reduce the size of your JavaScript files. With a smaller sizes file, users will be able to download the asset faster, resulting in improved performance.
|
||||
|
||||
- [Check GZIP compression](https://checkgzipcompression.com/)
|
||||
- [Check Brotli Compression](https://tools.keycdn.com/brotli-test)
|
||||
- [Can I use... Brotli](https://caniuse.com/#feat=brotli)
|
||||
@@ -0,0 +1,18 @@
|
||||
# Framework Guides
|
||||
|
||||
These guides are intended to help you optimize your application for performance when using a specific framework.
|
||||
|
||||
## Angular
|
||||
|
||||
- [Angular Performance Checklist](https://github.com/mgechev/angular-performance-checklist)
|
||||
|
||||
## React
|
||||
|
||||
- [Optimizing Performance - React](https://reactjs.org/docs/optimizing-performance.html)
|
||||
- [React image manipulation | Cloudinary](https://cloudinary.com/documentation/react_image_manipulation)
|
||||
- [Debugging React performance with React 16 and Chrome Devtools.](https://building.calibreapp.com/debugging-react-performance-with-react-16-and-chrome-devtools-c90698a522ad)
|
||||
- [Build Performant - React](https://web.dev/react/)
|
||||
|
||||
## Vue
|
||||
|
||||
- [Vue - Useful Links|Style Guide and Performance](https://learn-vuejs.github.io/vue-patterns/useful-links/)
|
||||
1
src/best-practices/frontend-performance/content/index.md
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,13 @@
|
||||
# Inline Critical CSS
|
||||
|
||||
> The CSS critical (or "above the fold") collects all the CSS used to render the visible portion of the page. It is embedded before your principal CSS call and between `<style></style>` in a single line (minified if possible).
|
||||
|
||||
Inlining critical CSS help to speed up the rendering of the web pages reducing the number of requests to the server.
|
||||
|
||||
Generate the CSS critical with online tools or using a plugin like the one that Addy Osmani developed.
|
||||
|
||||
- [Understanding Critical CSS](https://www.smashingmagazine.com/2015/08/understanding-critical-css/)
|
||||
- [Critical by Addy Osmani on GitHub](https://github.com/addyosmani/critical) automates this.
|
||||
- [Inlining critical CSS for better web performance | Go Make Things](https://gomakethings.com/inlining-critical-css-for-better-web-performance/)
|
||||
- [Critical Path CSS Generator - Prioritize above the fold content :: SiteLocity](https://www.sitelocity.com/critical-path-css-generator)
|
||||
- [Reduce the size of the above-the-fold content](https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Cookie Size
|
||||
|
||||
> If you are using cookies, be sure each cookie doesn't exceed 4096 bytes and your domain name doesn't have more than 20 cookies.
|
||||
|
||||
Cookies are exchanged in the HTTP headers between web servers and browsers. It's important to keep the size of cookies as low as possible to minimize the impact on the user's response time.
|
||||
|
||||
- [Cookie specification: RFC 6265](https://tools.ietf.org/html/rfc6265)
|
||||
- [Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)
|
||||
- [Browser Cookie Limits](http://browsercookielimits.squawky.net/)
|
||||
- [Website Performance: Cookies Don't Taste So Good - Monitis Blog](http://www.monitis.com/blog/website-performance-cookies-dont-taste-so-good/)
|
||||
- [Google's Web Performance Best Practices #3: Minimize Request Overhead - GlobalDots Blog](https://www.globaldots.com/googles-web-performance-best-practices-3-minimize-request-overhead/)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Dependency Updates
|
||||
|
||||
> All JavaScript libraries used in your project are necessary (prefer Vanilla JavaScript for simple functionalities), updated to their latest version and don't overwhelm your JavaScript with unnecessary methods.
|
||||
|
||||
Most of the time, new versions come with optimization and security fix. You should use the most optimized code to speed up your project and ensure that you'll not slow down your website or app without outdated plugin.
|
||||
|
||||
If your project use NPM packages, [npm-check](https://www.npmjs.com/package/npm-check) is a pretty interesting library to upgrade / update your libraries. [Greenkeeper](https://greenkeeper.io/) can automatically look for your dependencies and suggest an update every time a new version is out.
|
||||
|
||||
|
||||
- [Vanilla JavaScript for building powerful web applications](https://plainjs.com/)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Keep TTFB < 1.3s
|
||||
|
||||
Reduce as much as you can the time your browser waits before receiving data.
|
||||
|
||||
- [What is Waiting (TTFB) in DevTools, and what to do about it](https://scaleyourcode.com/blog/article/27)
|
||||
- [Monitoring your servers with free tools is easy](https://scaleyourcode.com/blog/article/7)
|
||||
- [Time to First Byte (TTFB)](https://varvy.com/pagespeed/ttfb.html)
|
||||
- [Global latency testing tool](https://latency.apex.sh)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Keep Web Font Size Under 300kb
|
||||
|
||||
Web fonts are a great way to add style to your website. However, they can also be a major performance bottleneck. The more fonts you use, the more time it takes for the browser to download and render the page. This can lead to a poor user experience and a high bounce rate.
|
||||
|
||||
Webfont sizes shouldn't exceed 300kb (all variants included) and are optimized for performance.
|
||||
|
||||
- [Font Bytes - Page Weight](https://httparchive.org/reports/page-weight#bytesFont)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Lighthouse
|
||||
|
||||
Lighthouse is an open-source tool developed by Google that is used to audit the performance, accessibility, and SEO of web pages. It is available as a browser extension and as a command-line tool, and it can be run on any web page to generate a report with recommendations for improvement.
|
||||
|
||||
Lighthouse works by simulating the load and interaction of a web page and measuring various performance metrics, such as load time, time to first paint, and time to interactive. It also checks for common issues such as incorrect image sizes, missing alt text, and broken links.
|
||||
|
||||
Lighthouse provides a comprehensive and easy-to-use tool for identifying and fixing performance and accessibility issues on web pages. It is widely used by web developers and is integrated into many popular development tools.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Lighthouse - Google Developers](https://developers.google.com/web/tools/lighthouse)
|
||||