mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 02:01:57 +08:00
Compare commits
56 Commits
4.0
...
content/sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83504f495b | ||
|
|
fc6e8048a7 | ||
|
|
8264c4509f | ||
|
|
fa9d8d6656 | ||
|
|
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 |
@@ -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 });
|
||||
163
bin/roadmap-content.cjs
Normal file
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/${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
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);
|
||||
});
|
||||
@@ -10,8 +10,10 @@
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.0.0",
|
||||
@@ -20,6 +22,7 @@
|
||||
"astro-compress": "^1.1.24",
|
||||
"astro-critters": "^1.1.24",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.1",
|
||||
"tailwindcss": "^3.2.4"
|
||||
|
||||
1402
pnpm-lock.yaml
generated
1402
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 835 KiB |
BIN
public/images/devops-ebook.png
Normal file
BIN
public/images/devops-ebook.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
1
public/jsons/spring-boot.json
Normal file
1
public/jsons/spring-boot.json
Normal file
File diff suppressed because one or more lines are too long
BIN
public/pdfs/software-architect.pdf
Normal file
BIN
public/pdfs/software-architect.pdf
Normal file
Binary file not shown.
BIN
public/pdfs/spring-boot.pdf
Normal file
BIN
public/pdfs/spring-boot.pdf
Normal file
Binary file not shown.
@@ -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,6 +48,7 @@ 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)
|
||||
@@ -2,7 +2,8 @@
|
||||
---
|
||||
|
||||
<script src='./analytics.js'></script>
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=UA-139582634-1'></script>
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=UA-139582634-1'
|
||||
></script>
|
||||
<script is:inline>
|
||||
// @ts-nocheck
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
@@ -12,4 +13,29 @@
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-139582634-1');
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
let trackEl = e.target;
|
||||
if (!trackEl.getAttribute('ga-category')) {
|
||||
trackEl = trackEl.closest('[ga-category]');
|
||||
}
|
||||
|
||||
if (!trackEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = trackEl.getAttribute('ga-category');
|
||||
const action = trackEl.getAttribute('ga-action');
|
||||
const label = trackEl.getAttribute('ga-label');
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.fireEvent({
|
||||
category,
|
||||
action,
|
||||
label,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -34,8 +34,21 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
type='submit'
|
||||
name='submit'
|
||||
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2'
|
||||
submit-download-form
|
||||
>
|
||||
Send Link
|
||||
</button>
|
||||
</form>
|
||||
</Popup>
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelector('[submit-download-form]')
|
||||
?.addEventListener('click', () => {
|
||||
window.fireEvent({
|
||||
category: 'Subscription',
|
||||
action: 'Submitted Popup Form',
|
||||
label: 'Download Roadmap Popup',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
3
src/components/FAQs/Answer.astro
Normal file
3
src/components/FAQs/Answer.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class='text-sm sm:text-base leading-relaxed text-left p-2 sm:p-4 text-md text-gray-800 border-t border-t-gray-300 bg-gray-100 rounded-bl-md rounded-br-md'>
|
||||
<slot />
|
||||
</div>
|
||||
13
src/components/FAQs/FAQs.astro
Normal file
13
src/components/FAQs/FAQs.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class='border-t bg-gray-100'>
|
||||
<div class='container'>
|
||||
<div class='flex justify-between relative -top-5'>
|
||||
<h1 class='text-sm sm:text-base font-medium py-1 px-3 border bg-white rounded-md'>
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class='flex flex-col gap-1 pb-8'>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
42
src/components/FAQs/Question.astro
Normal file
42
src/components/FAQs/Question.astro
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
import Icon from '../Icon.astro';
|
||||
|
||||
export interface Props {
|
||||
question: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
const { question, isActive = false } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class='faq-item bg-white border rounded-md hover:bg-gray-50 border-gray-300'
|
||||
>
|
||||
<button
|
||||
faq-question
|
||||
class='flex flex-row justify-between items-center p-2 sm:p-3 w-full'
|
||||
>
|
||||
<span class='text-sm sm:text-base text-left font-medium'>{question}</span>
|
||||
<Icon icon='down' class='h-6 hidden sm:block text-gray-400' />
|
||||
</button>
|
||||
<div class:list={['answer', { hidden: !isActive }]} faq-answer>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('[faq-question]').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
// Hide any other visible answers
|
||||
document.querySelectorAll('[faq-answer]').forEach((element) => {
|
||||
element.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Show the current answer
|
||||
const answer = el.nextElementSibling;
|
||||
if (answer) {
|
||||
answer.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -84,23 +84,29 @@ import Icon from './Icon.astro';
|
||||
<div class='text-gray-400 text-sm'>
|
||||
<p>
|
||||
<a
|
||||
href='https://thenewstack.io/category/devops/'
|
||||
href='https://thenewstack.io/category/devops?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Footer` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Footer'
|
||||
class='text-gray-400 hover:text-white'>DevOps</a
|
||||
>
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a
|
||||
href='https://thenewstack.io/category/kubernetes/'
|
||||
href='https://thenewstack.io/category/kubernetes?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Footer` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Footer'
|
||||
class='text-gray-400 hover:text-white'>Kubernetes</a
|
||||
>
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a
|
||||
href='https://thenewstack.io/category/cloud-native/'
|
||||
href='https://thenewstack.io/category/cloud-native?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Footer'
|
||||
target='_blank'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Footer` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Footer'
|
||||
class='text-gray-400 hover:text-white'>Cloud-Native</a
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,11 @@ const { author } = frontmatter;
|
||||
target='_blank'
|
||||
class='font-medium hover:text-gray-600 inline-flex items-center hover:underline'
|
||||
>
|
||||
<img src={author.imageUrl} class='w-5 h-5 inline mr-2 rounded-full' />
|
||||
<img
|
||||
alt={author.name}
|
||||
src={author.imageUrl}
|
||||
class='w-5 h-5 inline mr-2 rounded-full'
|
||||
/>
|
||||
{author.name}
|
||||
</a>
|
||||
<span class='mx-1.5'>·</span>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import DownloadPopup from "../DownloadPopup.astro";
|
||||
import Loader from "../Loader.astro";
|
||||
import ShareIcons from "../ShareIcons.astro";
|
||||
import SubscribePopup from "../SubscribePopup.astro";
|
||||
import TopicOverlay from "../TopicOverlay.astro";
|
||||
import "./InteractiveRoadmap.css";
|
||||
import DownloadPopup from '../DownloadPopup.astro';
|
||||
import Loader from '../Loader.astro';
|
||||
import ShareIcons from '../ShareIcons.astro';
|
||||
import SubscribePopup from '../SubscribePopup.astro';
|
||||
import TopicOverlay from '../TopicOverlay.astro';
|
||||
import './InteractiveRoadmap.css';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
@@ -16,31 +16,32 @@ export interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
const { roadmapId, jsonUrl, dimensions = null, description } =
|
||||
Astro.props;
|
||||
const { roadmapId, jsonUrl, dimensions = null, description } = Astro.props;
|
||||
---
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/balsamiq.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
rel='preload'
|
||||
href='/fonts/balsamiq.woff2'
|
||||
as='font'
|
||||
type='font/woff2'
|
||||
crossorigin
|
||||
slot="after-header"
|
||||
slot='after-header'
|
||||
/>
|
||||
|
||||
<div class="bg-gray-50 py-4 sm:py-12">
|
||||
<div class="max-w-[1000px] container relative">
|
||||
<div class='bg-gray-50 py-4 sm:py-12'>
|
||||
<div class='max-w-[1000px] container relative'>
|
||||
<ShareIcons
|
||||
description={description}
|
||||
pageUrl={`https://roadmap.sh/${roadmapId}`}
|
||||
/>
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
<TopicOverlay />
|
||||
<TopicOverlay roadmapId={roadmapId} />
|
||||
<div
|
||||
id="roadmap-svg"
|
||||
style={dimensions ? `--aspect-ratio:${dimensions.width}/${dimensions.height}` : null}
|
||||
id='roadmap-svg'
|
||||
style={dimensions
|
||||
? `--aspect-ratio:${dimensions.width}/${dimensions.height}`
|
||||
: null}
|
||||
data-roadmap-id={roadmapId}
|
||||
data-json-url={jsonUrl}
|
||||
>
|
||||
@@ -49,4 +50,4 @@ const { roadmapId, jsonUrl, dimensions = null, description } =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./roadmap.js"></script>
|
||||
<script src='./roadmap.js'></script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { wireframeJSONToSVG } from "roadmap-renderer";
|
||||
import { Topic } from "./topic";
|
||||
import { Sharer } from "./sharer";
|
||||
import { wireframeJSONToSVG } from 'roadmap-renderer';
|
||||
import { Topic } from './topic';
|
||||
import { Sharer } from './sharer';
|
||||
|
||||
/**
|
||||
* @typedef {{ roadmapId: string, jsonUrl: string }} RoadmapConfig
|
||||
@@ -11,10 +11,10 @@ export class Roadmap {
|
||||
* @param {RoadmapConfig} config
|
||||
*/
|
||||
constructor() {
|
||||
this.roadmapId = "";
|
||||
this.jsonUrl = "";
|
||||
this.roadmapId = '';
|
||||
this.jsonUrl = '';
|
||||
|
||||
this.containerId = "roadmap-svg";
|
||||
this.containerId = 'roadmap-svg';
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
||||
@@ -28,10 +28,16 @@ export class Roadmap {
|
||||
}
|
||||
|
||||
prepareConfig() {
|
||||
if (!this.containerEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataset = this.containerEl.dataset;
|
||||
|
||||
this.roadmapId = dataset.roadmapId;
|
||||
this.jsonUrl = dataset.jsonUrl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +46,7 @@ export class Roadmap {
|
||||
*/
|
||||
fetchRoadmapSvg(jsonUrl) {
|
||||
if (!jsonUrl) {
|
||||
console.error("jsonUrl not defined in frontmatter");
|
||||
console.error('jsonUrl not defined in frontmatter');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,13 +56,15 @@ export class Roadmap {
|
||||
})
|
||||
.then(function (json) {
|
||||
return wireframeJSONToSVG(json, {
|
||||
fontURL: "/fonts/balsamiq.woff2",
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onDOMLoaded() {
|
||||
this.prepareConfig();
|
||||
if (!this.prepareConfig()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchRoadmapSvg(this.jsonUrl)
|
||||
.then((svg) => {
|
||||
@@ -66,8 +74,8 @@ export class Roadmap {
|
||||
}
|
||||
|
||||
handleRoadmapClick(e) {
|
||||
const targetGroup = e.target.closest("g") || {};
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : "";
|
||||
const targetGroup = e.target.closest('g') || {};
|
||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +83,7 @@ export class Roadmap {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("topic.click", {
|
||||
new CustomEvent('topic.click', {
|
||||
detail: {
|
||||
topicId: groupId,
|
||||
roadmapId: this.roadmapId,
|
||||
@@ -85,8 +93,8 @@ export class Roadmap {
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("DOMContentLoaded", this.onDOMLoaded);
|
||||
window.addEventListener("click", this.handleRoadmapClick);
|
||||
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
|
||||
window.addEventListener('click', this.handleRoadmapClick);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export class Sharer {
|
||||
this.init = this.init.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
|
||||
this.shareIconsId = "page-share-icons";
|
||||
this.shareIconsId = 'page-share-icons';
|
||||
}
|
||||
|
||||
get shareIconsEl() {
|
||||
@@ -12,14 +12,18 @@ export class Sharer {
|
||||
|
||||
onScroll() {
|
||||
if (window.scrollY < 100 || window.innerWidth < 1050) {
|
||||
this.shareIconsEl.classList.add("hidden");
|
||||
this.shareIconsEl.classList.add('hidden');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.shareIconsEl.classList.remove("hidden");
|
||||
this.shareIconsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("scroll", this.onScroll, { passive: true });
|
||||
if (!this.shareIconsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', this.onScroll, { passive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,204 +1,219 @@
|
||||
export class Topic {
|
||||
constructor() {
|
||||
this.overlayId = 'topic-overlay';
|
||||
this.contentId = 'topic-content';
|
||||
this.loaderId = 'topic-loader';
|
||||
this.topicBodyId = 'topic-body';
|
||||
this.topicActionsId = 'topic-actions';
|
||||
this.markTopicDoneId = 'mark-topic-done';
|
||||
this.markTopicPendingId = 'mark-topic-pending';
|
||||
this.closeTopicId = 'close-topic';
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
|
||||
this.handleTopicClick = this.handleTopicClick.bind(this);
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
this.resetDOM = this.resetDOM.bind(this);
|
||||
this.populate = this.populate.bind(this);
|
||||
this.handleOverlayClick = this.handleOverlayClick.bind(this);
|
||||
this.markAsDone = this.markAsDone.bind(this);
|
||||
this.markAsPending = this.markAsPending.bind(this);
|
||||
this.queryRoadmapElementsByTopicId = this.queryRoadmapElementsByTopicId.bind(this);
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
constructor() {
|
||||
this.overlayId = 'topic-overlay';
|
||||
this.contentId = 'topic-content';
|
||||
this.loaderId = 'topic-loader';
|
||||
this.topicBodyId = 'topic-body';
|
||||
this.topicActionsId = 'topic-actions';
|
||||
this.markTopicDoneId = 'mark-topic-done';
|
||||
this.markTopicPendingId = 'mark-topic-pending';
|
||||
this.closeTopicId = 'close-topic';
|
||||
this.contributionTextId = 'contrib-meta';
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
|
||||
this.handleTopicClick = this.handleTopicClick.bind(this);
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
this.resetDOM = this.resetDOM.bind(this);
|
||||
this.populate = this.populate.bind(this);
|
||||
this.handleOverlayClick = this.handleOverlayClick.bind(this);
|
||||
this.markAsDone = this.markAsDone.bind(this);
|
||||
this.markAsPending = this.markAsPending.bind(this);
|
||||
this.queryRoadmapElementsByTopicId =
|
||||
this.queryRoadmapElementsByTopicId.bind(this);
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
}
|
||||
|
||||
get loaderEl() {
|
||||
return document.getElementById(this.loaderId);
|
||||
}
|
||||
|
||||
get markTopicDoneEl() {
|
||||
return document.getElementById(this.markTopicDoneId);
|
||||
}
|
||||
|
||||
get markTopicPendingEl() {
|
||||
return document.getElementById(this.markTopicPendingId);
|
||||
}
|
||||
|
||||
get topicActionsEl() {
|
||||
return document.getElementById(this.topicActionsId);
|
||||
}
|
||||
|
||||
get contributionTextEl() {
|
||||
return document.getElementById(this.contributionTextId);
|
||||
}
|
||||
|
||||
get contentEl() {
|
||||
return document.getElementById(this.contentId);
|
||||
}
|
||||
|
||||
get overlayEl() {
|
||||
return document.getElementById(this.overlayId);
|
||||
}
|
||||
|
||||
resetDOM(hideOverlay = false) {
|
||||
if (hideOverlay) {
|
||||
this.overlayEl.classList.add('hidden');
|
||||
} else {
|
||||
this.overlayEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
get loaderEl() {
|
||||
return document.getElementById(this.loaderId);
|
||||
}
|
||||
|
||||
get markTopicDoneEl() {
|
||||
return document.getElementById(this.markTopicDoneId);
|
||||
}
|
||||
|
||||
get markTopicPendingEl() {
|
||||
return document.getElementById(this.markTopicPendingId);
|
||||
}
|
||||
|
||||
get topicActionsEl() {
|
||||
return document.getElementById(this.topicActionsId);
|
||||
}
|
||||
|
||||
get contentEl() {
|
||||
return document.getElementById(this.contentId);
|
||||
}
|
||||
|
||||
get overlayEl() {
|
||||
return document.getElementById(this.overlayId);
|
||||
}
|
||||
|
||||
resetDOM(hideOverlay = false) {
|
||||
if (hideOverlay) {
|
||||
this.overlayEl.classList.add('hidden');
|
||||
} else {
|
||||
this.overlayEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
this.loaderEl.classList.remove('hidden'); // Show loader
|
||||
this.topicActionsEl.classList.add('hidden'); // Hide Actions
|
||||
this.contentEl.replaceChildren(''); // Remove content
|
||||
}
|
||||
|
||||
close() {
|
||||
this.resetDOM(true);
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | HTMLElement} html
|
||||
*/
|
||||
populate(html) {
|
||||
this.contentEl.replaceChildren(html);
|
||||
this.loaderEl.classList.add('hidden');
|
||||
this.topicActionsEl.classList.remove('hidden');
|
||||
|
||||
const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, '');
|
||||
const isDone = localStorage.getItem(normalizedGroup) === 'done';
|
||||
|
||||
if (isDone) {
|
||||
this.markTopicDoneEl.classList.add('hidden');
|
||||
this.markTopicPendingEl.classList.remove('hidden');
|
||||
} else {
|
||||
this.markTopicDoneEl.classList.remove('hidden');
|
||||
this.markTopicPendingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
fetchTopicHtml(roadmapId, topicId) {
|
||||
const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/');
|
||||
const fullUrl = `/${roadmapId}/${topicPartial}/`;
|
||||
|
||||
return fetch(fullUrl)
|
||||
.then((res) => {
|
||||
return res.text();
|
||||
})
|
||||
.then((topicHtml) => {
|
||||
// It's full HTML with page body, head etc.
|
||||
// We only need the inner HTML of the #main-content
|
||||
const node = new DOMParser().parseFromString(topicHtml, 'text/html');
|
||||
|
||||
return node.getElementById('main-content');
|
||||
});
|
||||
}
|
||||
|
||||
handleTopicClick(e) {
|
||||
const { roadmapId, topicId } = e.detail;
|
||||
if (!topicId || !roadmapId) {
|
||||
console.log('Missing topic or roadmap: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeRoadmapId = roadmapId;
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
if (/^ext_link/.test(topicId)) {
|
||||
window.open(`https://${topicId.replace('ext_link:', '')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetDOM();
|
||||
this.fetchTopicHtml(roadmapId, topicId)
|
||||
.then((content) => {
|
||||
this.populate(content);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.populate('Error loading the content!');
|
||||
});
|
||||
}
|
||||
|
||||
queryRoadmapElementsByTopicId(topicId) {
|
||||
const elements = document.querySelectorAll(`[data-group-id$="-${topicId}"]`);
|
||||
const matchingElements = [];
|
||||
|
||||
elements.forEach((element) => {
|
||||
const foundGroupId = element?.dataset?.groupId || '';
|
||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
|
||||
|
||||
if (validGroupRegex.test(foundGroupId)) {
|
||||
matchingElements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
return matchingElements;
|
||||
}
|
||||
|
||||
markAsDone(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
localStorage.setItem(updatedTopicId, 'done');
|
||||
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.add('done');
|
||||
});
|
||||
}
|
||||
|
||||
markAsPending(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
|
||||
localStorage.removeItem(updatedTopicId);
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.remove('done');
|
||||
});
|
||||
}
|
||||
|
||||
handleOverlayClick(e) {
|
||||
const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`);
|
||||
|
||||
if (!isClickedInsideTopic) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isClickedDone = e.target.id === this.markTopicDoneId || e.target.closest(`#${this.markTopicDoneId}`);
|
||||
if (isClickedDone) {
|
||||
this.markAsDone(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedPending = e.target.id === this.markTopicPendingId || e.target.closest(`#${this.markTopicPendingId}`);
|
||||
if (isClickedPending) {
|
||||
this.markAsPending(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedClose = e.target.id === this.closeTopicId || e.target.closest(`#${this.closeTopicId}`);
|
||||
if (isClickedClose) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('topic.click', this.handleTopicClick);
|
||||
window.addEventListener('click', this.handleOverlayClick);
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === 'escape') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
this.loaderEl.classList.remove('hidden'); // Show loader
|
||||
this.topicActionsEl.classList.add('hidden'); // Hide Actions
|
||||
this.contributionTextEl.classList.add('hidden'); // Hide contribution text
|
||||
this.contentEl.replaceChildren(''); // Remove content
|
||||
}
|
||||
|
||||
close() {
|
||||
this.resetDOM(true);
|
||||
|
||||
this.activeRoadmapId = null;
|
||||
this.activeTopicId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | HTMLElement} html
|
||||
*/
|
||||
populate(html) {
|
||||
this.contentEl.replaceChildren(html);
|
||||
this.loaderEl.classList.add('hidden');
|
||||
this.topicActionsEl.classList.remove('hidden');
|
||||
this.contributionTextEl.classList.remove('hidden');
|
||||
|
||||
const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, '');
|
||||
const isDone = localStorage.getItem(normalizedGroup) === 'done';
|
||||
|
||||
if (isDone) {
|
||||
this.markTopicDoneEl.classList.add('hidden');
|
||||
this.markTopicPendingEl.classList.remove('hidden');
|
||||
} else {
|
||||
this.markTopicDoneEl.classList.remove('hidden');
|
||||
this.markTopicPendingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fetchTopicHtml(roadmapId, topicId) {
|
||||
const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/');
|
||||
const fullUrl = `/${roadmapId}/${topicPartial}/`;
|
||||
|
||||
return fetch(fullUrl)
|
||||
.then((res) => {
|
||||
return res.text();
|
||||
})
|
||||
.then((topicHtml) => {
|
||||
// It's full HTML with page body, head etc.
|
||||
// We only need the inner HTML of the #main-content
|
||||
const node = new DOMParser().parseFromString(topicHtml, 'text/html');
|
||||
|
||||
return node.getElementById('main-content');
|
||||
});
|
||||
}
|
||||
|
||||
handleTopicClick(e) {
|
||||
const { roadmapId, topicId } = e.detail;
|
||||
if (!topicId || !roadmapId) {
|
||||
console.log('Missing topic or roadmap: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeRoadmapId = roadmapId;
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
if (/^ext_link/.test(topicId)) {
|
||||
window.open(`https://${topicId.replace('ext_link:', '')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetDOM();
|
||||
this.fetchTopicHtml(roadmapId, topicId)
|
||||
.then((content) => {
|
||||
this.populate(content);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.populate('Error loading the content!');
|
||||
});
|
||||
}
|
||||
|
||||
queryRoadmapElementsByTopicId(topicId) {
|
||||
const elements = document.querySelectorAll(
|
||||
`[data-group-id$="-${topicId}"]`
|
||||
);
|
||||
const matchingElements = [];
|
||||
|
||||
elements.forEach((element) => {
|
||||
const foundGroupId = element?.dataset?.groupId || '';
|
||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
|
||||
|
||||
if (validGroupRegex.test(foundGroupId)) {
|
||||
matchingElements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
return matchingElements;
|
||||
}
|
||||
|
||||
markAsDone(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
localStorage.setItem(updatedTopicId, 'done');
|
||||
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.add('done');
|
||||
});
|
||||
}
|
||||
|
||||
markAsPending(topicId) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
|
||||
localStorage.removeItem(updatedTopicId);
|
||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.remove('done');
|
||||
});
|
||||
}
|
||||
|
||||
handleOverlayClick(e) {
|
||||
const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`);
|
||||
|
||||
if (!isClickedInsideTopic) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isClickedDone =
|
||||
e.target.id === this.markTopicDoneId ||
|
||||
e.target.closest(`#${this.markTopicDoneId}`);
|
||||
if (isClickedDone) {
|
||||
this.markAsDone(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedPending =
|
||||
e.target.id === this.markTopicPendingId ||
|
||||
e.target.closest(`#${this.markTopicPendingId}`);
|
||||
if (isClickedPending) {
|
||||
this.markAsPending(this.activeTopicId);
|
||||
this.close();
|
||||
}
|
||||
|
||||
const isClickedClose =
|
||||
e.target.id === this.closeTopicId ||
|
||||
e.target.closest(`#${this.closeTopicId}`);
|
||||
if (isClickedClose) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('topic.click', this.handleTopicClick);
|
||||
window.addEventListener('click', this.handleOverlayClick);
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === 'escape') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
---
|
||||
import "../styles/prism.css";
|
||||
import '../styles/prism.css';
|
||||
import DownloadPopup from './DownloadPopup.astro';
|
||||
import ShareIcons from './ShareIcons.astro';
|
||||
import SubscribePopup from './SubscribePopup.astro';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { roadmapId, description } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="bg-gray-50 py-4 sm:py-10">
|
||||
<div class="container prose prose-headings:mt-4 prose-headings:mb-2 prose-p:mb-0.5">
|
||||
<div class='bg-gray-50 py-4 sm:py-10'>
|
||||
<div
|
||||
class='container prose prose-headings:mt-4 prose-headings:mb-2 prose-p:mb-0.5 relative prose-code:text-white'
|
||||
>
|
||||
<DownloadPopup />
|
||||
<SubscribePopup />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,24 +32,22 @@ import Icon from './Icon.astro';
|
||||
|
||||
<!-- Mobile Navigation Button -->
|
||||
<button
|
||||
id='show-mobile-navigation'
|
||||
class='text-gray-400 hover:text-gray-50 block sm:hidden cursor-pointer'
|
||||
aria-label='Menu'
|
||||
onclick="document.getElementById('mobile-navigation').classList.remove('hidden');"
|
||||
show-mobile-nav
|
||||
>
|
||||
<Icon icon='hamburger' />
|
||||
</button>
|
||||
|
||||
<!-- Mobile Navigation Items -->
|
||||
<div
|
||||
id='mobile-navigation'
|
||||
class='fixed top-0 bottom-0 left-0 right-0 z-40 bg-slate-900 items-center flex hidden'
|
||||
mobile-nav
|
||||
>
|
||||
<button
|
||||
id='close-mobile-navigation'
|
||||
close-mobile-nav
|
||||
class='text-gray-400 hover:text-gray-50 block cursor-pointer absolute top-6 right-6'
|
||||
aria-label='Close Menu'
|
||||
onclick="document.getElementById('mobile-navigation').classList.add('hidden');"
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
@@ -80,3 +78,15 @@ import Icon from './Icon.astro';
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('[show-mobile-nav]')?.addEventListener('click', () => {
|
||||
document.querySelector('[mobile-nav]')?.classList.remove('hidden');
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector('[close-mobile-nav]')
|
||||
?.addEventListener('click', () => {
|
||||
document.querySelector('[mobile-nav]')?.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@ const starCount = await getFormattedStars('kamranahmedse/developer-roadmap');
|
||||
|
||||
<div class='py-6 sm:py-16 border-b border-t text-left sm:text-center bg-white'>
|
||||
<div class='max-w-[600px] container'>
|
||||
<h1 class='text-3xl sm:text-5xl font-bold'>Open Source</h1>
|
||||
<h2 class='text-2xl sm:text-5xl font-bold'>Open Source</h2>
|
||||
<p class='text-gray-600 text-sm sm:text-lg leading-relaxed my-2.5 sm:my-5'>
|
||||
The project is OpenSource, <a
|
||||
href='https://github.com/search?o=desc&q=stars%3A%3E100000&s=stars&type=Repositories'
|
||||
|
||||
@@ -21,8 +21,7 @@ const { id, title, subtitle } = Astro.props;
|
||||
<div class='relative bg-white rounded-lg shadow popup-body'>
|
||||
<button
|
||||
type='button'
|
||||
class='absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center'
|
||||
onclick='this.closest(".popup").classList.add("hidden")'
|
||||
class='absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center popup-close'
|
||||
>
|
||||
<Icon icon='close' />
|
||||
<span class='sr-only'>Close popup</span>
|
||||
|
||||
@@ -31,8 +31,9 @@ export class Popup {
|
||||
const target = e.target;
|
||||
const popupBody = target.closest('.popup-body');
|
||||
const closestPopup = target.closest('.popup');
|
||||
const closeBtn = target.closest('.popup-close');
|
||||
|
||||
if (popupBody) {
|
||||
if (!closeBtn && popupBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const roadmapTitle =
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'mt-4 sm:mt-7 border rounded-md mb-0',
|
||||
'mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0',
|
||||
{
|
||||
'sm:-mb-[82px]': hasTNSBanner,
|
||||
'sm:-mb-[65px]': !hasTNSBanner,
|
||||
@@ -29,10 +29,12 @@ const roadmapTitle =
|
||||
<p class='text-sm'>
|
||||
Get the latest {roadmapTitle} news from our sister site{' '}
|
||||
<a
|
||||
href='https://thenewstack.io'
|
||||
href='https://thenewstack.io?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Alert'
|
||||
target='_blank'
|
||||
class='font-semibold underline'
|
||||
onclick="window.fireEvent({ category: 'PartnerClick', action: `TNS Referral`, label: `TNS Referral - Roadmap` })"
|
||||
ga-category='PartnerClick'
|
||||
ga-action='TNS Referral'
|
||||
ga-label='TNS Referral - Roadmap'
|
||||
>
|
||||
TheNewStack.io
|
||||
</a>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
import ResourcesAlert from "./ResourcesAlert.astro";
|
||||
import TopicSearch from "./TopicSearch/TopicSearch.astro";
|
||||
import YouTubeAlert from "./YouTubeAlert.astro";
|
||||
import Icon from './Icon.astro';
|
||||
import ResourcesAlert from './ResourcesAlert.astro';
|
||||
import TopicSearch from './TopicSearch/TopicSearch.astro';
|
||||
import YouTubeAlert from './YouTubeAlert.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -22,48 +22,57 @@ const {
|
||||
hasTopics = false,
|
||||
} = Astro.props;
|
||||
|
||||
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
---
|
||||
|
||||
<div class="border-b">
|
||||
<div class="py-5 sm:py-12 container relative">
|
||||
<div class='border-b'>
|
||||
<div class='py-5 sm:py-12 container relative'>
|
||||
<YouTubeAlert />
|
||||
|
||||
<div class="mt-0 mb-3 sm:mb-6 sm:mt-4">
|
||||
<h1 class="text-2xl sm:text-4xl mb-0.5 sm:mb-2 font-bold">
|
||||
<div class='mt-0 mb-3 sm:mb-6 sm:mt-4'>
|
||||
<h1 class='text-2xl sm:text-4xl mb-0.5 sm:mb-2 font-bold'>
|
||||
{title}
|
||||
</h1>
|
||||
<p class="text-gray-500 text-sm sm:text-lg">{description}</p>
|
||||
<p class='text-gray-500 text-sm sm:text-lg'>{description}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-1 sm:gap-2">
|
||||
<div class='flex justify-between'>
|
||||
<div class='flex gap-1 sm:gap-2'>
|
||||
{
|
||||
!hasSearch && (
|
||||
<>
|
||||
<a href='/roadmaps' class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600' aria-label="Back to All Roadmaps">
|
||||
←<span class='hidden sm:inline'> All Roadmaps</span>
|
||||
</a>
|
||||
<a
|
||||
href='/roadmaps/'
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
aria-label='Back to All Roadmaps'
|
||||
>
|
||||
←<span class='hidden sm:inline'> All Roadmaps</span>
|
||||
</a>
|
||||
|
||||
{isRoadmapReady && (
|
||||
<button
|
||||
data-popup="download-popup"
|
||||
class="inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500"
|
||||
aria-label="Download Roadmap"
|
||||
data-popup='download-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
aria-label='Download Roadmap'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Download Roadmap Popup'
|
||||
>
|
||||
<Icon icon="download" />
|
||||
<span class="hidden sm:inline ml-2">Download</span>
|
||||
<Icon icon='download' />
|
||||
<span class='hidden sm:inline ml-2'>Download</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
data-popup="subscribe-popup"
|
||||
class="inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500"
|
||||
aria-label="Subscribe for Updates"
|
||||
data-popup='subscribe-popup'
|
||||
class='inline-flex items-center justify-center bg-yellow-400 py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-yellow-500'
|
||||
aria-label='Subscribe for Updates'
|
||||
ga-category='Subscription'
|
||||
ga-action='Clicked Popup Opener'
|
||||
ga-label='Subscribe Roadmap Popup'
|
||||
>
|
||||
<Icon icon="email" />
|
||||
<span class="ml-2">Subscribe</span>
|
||||
<Icon icon='email' />
|
||||
<span class='ml-2'>Subscribe</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
@@ -73,11 +82,11 @@ const isRoadmapReady = !isUpcoming;
|
||||
hasSearch && (
|
||||
<a
|
||||
href={`/${roadmapId}/`}
|
||||
class="bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600"
|
||||
aria-label="Back to Visual Roadmap"
|
||||
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
|
||||
aria-label='Back to Visual Roadmap'
|
||||
>
|
||||
←
|
||||
<span class="inline"> Visual Roadmap</span>
|
||||
<span class='inline'> Visual Roadmap</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -87,13 +96,13 @@ const isRoadmapReady = !isUpcoming;
|
||||
isRoadmapReady && (
|
||||
<a
|
||||
href={`https://github.com/kamranahmedse/developer-roadmap/issues/new?title=[Suggestion] ${title}`}
|
||||
target="_blank"
|
||||
class="inline-flex items-center justify-center bg-gray-500 text-white py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-gray-600"
|
||||
aria-label="Suggest Changes"
|
||||
target='_blank'
|
||||
class='inline-flex items-center justify-center bg-gray-500 text-white py-1.5 px-3 text-xs sm:text-sm font-medium rounded-md hover:bg-gray-600'
|
||||
aria-label='Suggest Changes'
|
||||
>
|
||||
<Icon icon="comment" class="h-3 w-3" />
|
||||
<span class="ml-2 hidden sm:inline">Suggest Changes</span>
|
||||
<span class="ml-2 inline sm:hidden">Suggest</span>
|
||||
<Icon icon='comment' class='h-3 w-3' />
|
||||
<span class='ml-2 hidden sm:inline'>Suggest Changes</span>
|
||||
<span class='ml-2 inline sm:hidden'>Suggest</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import type { GAEventType } from '../Analytics/analytics';
|
||||
import Icon from '../Icon.astro';
|
||||
|
||||
export type SponsorType = {
|
||||
url: string;
|
||||
@@ -25,10 +26,19 @@ const {
|
||||
id='sponsor-ad'
|
||||
target='_blank'
|
||||
rel='noopener sponsored'
|
||||
onclick={event ? `window.fireEvent(${JSON.stringify(event)})` : ''}
|
||||
ga-category={event?.category}
|
||||
ga-action={event?.action}
|
||||
ga-label={event?.label}
|
||||
class='fixed bottom-[15px] right-[20px] outline-transparent z-50 bg-white max-w-[330px] shadow-lg outline-0 hidden'
|
||||
>
|
||||
<img src={imageUrl} class='w-[100px] lg:w-[130px]' alt='Sponsor Banner' />
|
||||
<button
|
||||
class='absolute top-1.5 right-1.5 text-gray-300 hover:text-gray-800'
|
||||
aria-label='Close'
|
||||
close-sponsor
|
||||
>
|
||||
<Icon icon='close' class='h-4' />
|
||||
</button>
|
||||
<img src={imageUrl} class='h-[150px] lg:h-[169px]' alt='Sponsor Banner' />
|
||||
<span class='text-sm flex flex-col justify-between'>
|
||||
<span class='p-[10px]'>
|
||||
<span class='font-semibold mb-0.5 block'>{title}</span>
|
||||
@@ -37,3 +47,11 @@ const {
|
||||
<span class='sponsor-footer'>Partner Content</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<script>
|
||||
document.querySelector('[close-sponsor]')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
document.getElementById('sponsor-ad')?.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -33,6 +33,9 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
type='submit'
|
||||
name='submit'
|
||||
class='text-white bg-gradient-to-r from-amber-700 to-blue-800 hover:from-amber-800 hover:to-blue-900 font-regular rounded-md text-md px-5 py-2.5 w-full text-center mr-2'
|
||||
ga-category='Subscription'
|
||||
ga-action='Submitted Popup Form'
|
||||
ga-label='Subscribe Roadmap Popup'
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
|
||||
@@ -1,31 +1,69 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
import Loader from "./Loader.astro";
|
||||
import Icon from './Icon.astro';
|
||||
import Loader from './Loader.astro';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
}
|
||||
|
||||
const { roadmapId } = Astro.props;
|
||||
const githubLink = `https://github.com/kamranahmedse/developer-roadmap/tree/master/src/roadmaps/${roadmapId}/content`;
|
||||
---
|
||||
|
||||
<div id='topic-overlay' class='hidden'>
|
||||
<div class="fixed top-0 right-0 z-40 h-screen p-4 sm:p-6 overflow-y-auto bg-white w-full sm:max-w-[600px]" tabindex="-1" id='topic-body'>
|
||||
<div id='topic-loader' class='hidden'>
|
||||
<Loader />
|
||||
</div>
|
||||
|
||||
<div id='topic-actions' class='hidden mb-2'>
|
||||
<button id='mark-topic-done' class='bg-green-600 text-white p-1 px-2 text-sm rounded-md hover:bg-green-700 inline-flex items-center'>
|
||||
<Icon icon="check" /> <span class='ml-2'>Mark as Done</span>
|
||||
</button>
|
||||
|
||||
<button id='mark-topic-pending' class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 inline-flex items-center'>
|
||||
<Icon icon="reset" /> <span class='ml-2'>Mark as Pending</span>
|
||||
</button>
|
||||
|
||||
<button type="button" id='close-topic' class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 right-2.5 inline-flex items-center">
|
||||
<Icon icon="close" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id='topic-content' class='prose prose-h1:mt-7 prose-h1:mb-2.5 prose-p:mt-0 prose-p:mb-2 prose-li:m-0 prose-li:mb-0.5 prose-h2:mb-3 prose-h2:mt-0'></div>
|
||||
<div
|
||||
class='fixed top-0 right-0 z-40 h-screen p-4 sm:p-6 overflow-y-auto bg-white w-full sm:max-w-[600px]'
|
||||
tabindex='-1'
|
||||
id='topic-body'
|
||||
>
|
||||
<div id='topic-loader' class='hidden'>
|
||||
<Loader />
|
||||
</div>
|
||||
<div class="bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-30"></div>
|
||||
|
||||
<div id='topic-actions' class='hidden mb-2'>
|
||||
<button
|
||||
id='mark-topic-done'
|
||||
class='bg-green-600 text-white p-1 px-2 text-sm rounded-md hover:bg-green-700 inline-flex items-center'
|
||||
>
|
||||
<Icon icon='check' />
|
||||
<span class='ml-2'>Mark as Done</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id='mark-topic-pending'
|
||||
class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 inline-flex items-center'
|
||||
>
|
||||
<Icon icon='reset' />
|
||||
<span class='ml-2'>Mark as Pending</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
id='close-topic'
|
||||
class='text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 right-2.5 inline-flex items-center'
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id='topic-content'
|
||||
class='prose prose-h1:mt-7 prose-h1:mb-2.5 prose-p:mt-0 prose-p:mb-2 prose-li:m-0 prose-li:mb-0.5 prose-h2:mb-3 prose-h2:mt-0'
|
||||
>
|
||||
</div>
|
||||
|
||||
<p
|
||||
id='contrib-meta'
|
||||
class='text-gray-400 text-sm border-t pt-3 mt-10 hidden'
|
||||
>
|
||||
We are still working on this page. You can contribute by submitting a
|
||||
brief description and a few links to learn more about this topic <a
|
||||
target='_blank'
|
||||
class='underline text-blue-700'
|
||||
href={githubLink}>on GitHub repository.</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class='bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-30'>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,11 @@ const { author } = frontmatter;
|
||||
target='_blank'
|
||||
class='font-medium hover:text-gray-600 inline-flex items-center hover:underline'
|
||||
>
|
||||
<img src={author.imageUrl} class='w-5 h-5 inline mr-2 rounded-full' />
|
||||
<img
|
||||
alt={author.name}
|
||||
src={author.imageUrl}
|
||||
class='w-5 h-5 inline mr-2 rounded-full'
|
||||
/>
|
||||
{author.name}
|
||||
</a>
|
||||
<span class='mx-1.5'>·</span>
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
---
|
||||
import Icon from "./Icon.astro";
|
||||
import Icon from './Icon.astro';
|
||||
---
|
||||
|
||||
<div class='sticky top-0 border-b border-b-yellow-300 z-10 flex h-[37px]' id='sticky-youtube-banner'>
|
||||
<a href='https://youtube.com/theroadmap?sub_confirmation=1' target='_blank' class='flex bg-yellow-200 text-center flex-1 items-center justify-center text-sm hover:bg-yellow-300 outline-0 '>
|
||||
<Icon icon="youtube" class="mr-2" /> We now have a YouTube Channel. <span class='hidden sm:inline'>Subscribe for the video content.</span>
|
||||
</a>
|
||||
<button class='text-yellow-500 bg-yellow-200 hover:text-yellow-900 hover:bg-yellow-400 outline-0 px-2' onclick='this.parentElement.classList.add("hidden")' aria-label="Close">
|
||||
<Icon icon="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class='sticky top-0 border-b border-b-yellow-300 z-10 flex h-[37px]'
|
||||
youtube-banner
|
||||
>
|
||||
<a
|
||||
href='https://youtube.com/theroadmap?sub_confirmation=1'
|
||||
target='_blank'
|
||||
class='flex bg-yellow-200 text-center flex-1 items-center justify-center text-sm hover:bg-yellow-300 outline-0'
|
||||
>
|
||||
<Icon icon='youtube' class='mr-2' /> We now have a YouTube Channel. <span
|
||||
class='hidden sm:inline'>Subscribe for the video content.</span
|
||||
>
|
||||
</a>
|
||||
<button
|
||||
class='text-yellow-500 bg-yellow-200 hover:text-yellow-900 hover:bg-yellow-400 outline-0 px-2'
|
||||
aria-label='Close'
|
||||
close-youtube-banner
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelector('[close-youtube-banner]')
|
||||
?.addEventListener('click', () => {
|
||||
document.querySelector('[youtube-banner]').remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
8
src/env.d.ts
vendored
8
src/env.d.ts
vendored
@@ -1 +1,9 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
GITHUB_SHA: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
Asymptotic notation is the standard way of measuring the time and space that an algorithm will consume as the input grows. In one of my last guides, I covered "Big-O notation" and a lot of you asked for a similar one for Asymptotic notation. You can find the [previous guide here](/guides/big-o-notation).
|
||||
|
||||
[](/guides/asymptotic-notation.png)
|
||||
[](/guides/asymptotic-notation.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/avoid-render-blocking-javascript-with-async-defer.png)
|
||||
[](/guides/avoid-render-blocking-javascript-with-async-defer.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/basic-authentication.png)
|
||||
[](/guides/basic-authentication.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
Big-O notation is the mathematical notation that helps analyse the algorithms to get an idea about how they might perform as the input grows. The image below explains Big-O in a simple way without using any fancy terminology.
|
||||
|
||||
[](/guides/big-o-notation.png)
|
||||
[](/guides/big-o-notation.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/character-encodings.png)
|
||||
[](/guides/character-encodings.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
The image below details the differences between the continuous integration and continuous delivery. Also, here is the [accompanying video on implementing that with GitHub actions](https://www.youtube.com/watch?v=nyKZTKQS_EQ).
|
||||
|
||||
[](/guides/ci-cd.png)
|
||||
[](/guides/ci-cd.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/dhcp.png)
|
||||
[](/guides/dhcp.png)
|
||||
|
||||
|
||||
@@ -24,4 +24,4 @@ DNS or Domain Name System is one of the fundamental blocks of the internet. As a
|
||||
|
||||
DNS at its simplest is like a phonebook on your mobile phone. Whenever you have to call one of your contacts, you can either dial their number from your memory or use their name which will then be used by your mobile phone to search their number in your phone book to call them. Every time you make a new friend, or your existing friend gets a mobile phone, you have to memorize their phone number or save it in your phonebook to be able to call them later on. DNS or Domain Name System, in a similar fashion, is a mechanism that allows you to browse websites on the internet. Just like your mobile phone does not know how to call without knowing the phone number, your browser does not know how to open a website just by the domain name; it needs to know the IP Address for the website to open. You can either type the IP Address to open, or provide the domain name and press enter which will then be used by your browser to find the IP address by going through several hoops. The picture below is the illustration of how your browser finds a website on the internet.
|
||||
|
||||
[](https://i.imgur.com/z9rwm5A.png)
|
||||
[](https://i.imgur.com/z9rwm5A.png)
|
||||
|
||||
@@ -31,7 +31,7 @@ Given the name "Basic Authentication", you should not confuse Basic Authenticati
|
||||
|
||||
Because it is a part of the HTTP specifications, all the browsers have native support for "HTTP Basic Authentication". Given below is the screenshot from the implementation in Google Chrome.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## How does it Work?
|
||||
@@ -52,7 +52,7 @@ The browser might use Realm to cache the credential. In the future, when there i
|
||||
## Step 2
|
||||
Upon receiving the response from the server, the browser will notice the `WWW-Authenticate` header and will show the authentication popup.
|
||||
|
||||

|
||||

|
||||
|
||||
## Step 3
|
||||
After the user submits the credentials through this authentication popup, the browser will automatically encode the credentials using the `base64` encoding and send them in the `Authorization` header of the same request.
|
||||
|
||||
@@ -39,7 +39,7 @@ Before we get into further details, let me give you an overview of the terms tha
|
||||
- **Cache Validation** is the process of contacting the server to check the validity of the cached content and get it updated for when it is going to expire
|
||||
- **Cache Invalidation** is the process of removing any stale content available in the cache
|
||||
|
||||

|
||||

|
||||
|
||||
### Caching Locations
|
||||
|
||||
@@ -75,21 +75,30 @@ Although you can control the reverse proxy caches (since it is implemented by yo
|
||||
|
||||
So, how do we control the web cache? Whenever the server emits some response, it is accompanied by some HTTP headers to guide the caches on whether and how to cache this response. The content provider is the one that has to make sure to return proper HTTP headers to force the caches on how to cache the content.
|
||||
|
||||
- [Expires](#expires)
|
||||
- [Pragma](#pragma)
|
||||
- [Cache-Control](#cache-control)
|
||||
- [private](#private)
|
||||
- [public](#public)
|
||||
- [no-store](#no-store)
|
||||
- [no-cache](#no-cache)
|
||||
- [max-age: seconds](#max-age)
|
||||
- [s-maxage: seconds](#s-maxage)
|
||||
- [must-revalidate](#must-revalidate)
|
||||
- [proxy-revalidate](#proxy-revalidate)
|
||||
- [Mixing Values](#mixing-values)
|
||||
- [Introduction](#introduction)
|
||||
- [Caching Locations](#caching-locations)
|
||||
- [Browser Cache](#browser-cache)
|
||||
- [Proxy Cache](#proxy-cache)
|
||||
- [Reverse Proxy Cache](#reverse-proxy-cache)
|
||||
- [Caching Headers](#caching-headers)
|
||||
- [Expires](#expires)
|
||||
- [Pragma](#pragma)
|
||||
- [Cache-Control](#cache-control)
|
||||
- [private](#private)
|
||||
- [public](#public)
|
||||
- [no-store](#no-store)
|
||||
- [no-cache](#no-cache)
|
||||
- [max-age: seconds](#max-age-seconds)
|
||||
- [s-maxage: seconds](#s-maxage-seconds)
|
||||
- [must-revalidate](#must-revalidate)
|
||||
- [proxy-revalidate](#proxy-revalidate)
|
||||
- [Mixing Values](#mixing-values)
|
||||
- [Validators](#validators)
|
||||
- [ETag](#etag)
|
||||
- [Last-Modified](#last-modified)
|
||||
- [Where do I start?](#where-do-i-start)
|
||||
- [Utilizing Server](#utilizing-server)
|
||||
- [Caching Recommendations](#caching-recommendations)
|
||||
|
||||
#### Expires
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ Three-way handshake in its simplest form is that all the `TCP` connections begin
|
||||
|
||||
Once the three-way handshake is completed, the data sharing between the client and server may begin. It should be noted that the client may start sending the application data as soon as it dispatches the last `ACK` packet but the server will still have to wait for the `ACK` packet to be received in order to fulfill the request.
|
||||
|
||||

|
||||

|
||||
|
||||
> Please note that there is a minor issue with the image, the last `ACK` packet sent by the client to end the handshake contains only `y+1` i.e. it should have been `ACK:y+1` instead of `ACK: x+1, y+1`
|
||||
|
||||
@@ -131,7 +131,7 @@ After merely 3 years of `HTTP/1.0`, the next version i.e. `HTTP/1.1` was release
|
||||
- New status codes
|
||||
- ..and more
|
||||
|
||||
I am not going to dwell about all the `HTTP/1.1` features in this post as it is a topic in itself and you can already find a lot about it. The one such document that I would recommend you to read is [Key differences between `HTTP/1.0` and HTTP/1.1](http://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf) and here is the link to [original RFC](https://tools.ietf.org/html/rfc2616) for the overachievers.
|
||||
I am not going to dwell about all the `HTTP/1.1` features in this post as it is a topic in itself and you can already find a lot about it. The one such document that I would recommend you to read is [Key differences between `HTTP/1.0` and HTTP/1.1](https://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf) and here is the link to [original RFC](https://tools.ietf.org/html/rfc2616) for the overachievers.
|
||||
|
||||
`HTTP/1.1` was introduced in 1999 and it had been a standard for many years. Although, it improved alot over its predecessor; with the web changing everyday, it started to show its age. Loading a web page these days is more resource-intensive than it ever was. A simple webpage these days has to open more than 30 connections. Well `HTTP/1.1` has persistent connections, then why so many connections? you say! The reason is, in `HTTP/1.1` it can only have one outstanding connection at any moment of time. `HTTP/1.1` tried to fix this by introducing pipelining but it didn't completely address the issue because of the **head-of-line blocking** where a slow or heavy request may block the requests behind and once a request gets stuck in a pipeline, it will have to wait for the next requests to be fulfilled. To overcome these shortcomings of `HTTP/1.1`, the developers started implementing the workarounds, for example use of spritesheets, encoded images in CSS, single humongous CSS/Javascript files, [domain sharding](https://www.maxcdn.com/one/visual-glossary/domain-sharding-2/) etc.
|
||||
|
||||
@@ -162,7 +162,7 @@ By now, you must be convinced that why we needed another revision of the HTTP pr
|
||||
- Request Prioritization
|
||||
- Security
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
#### 1. Binary Protocol
|
||||
@@ -187,7 +187,7 @@ Since `HTTP/2` is now a binary protocol and as I said above that it uses frames
|
||||
|
||||
It was part of a separate RFC which was specifically aimed at optimizing the sent headers. The essence of it is that when we are constantly accessing the server from a same client there is alot of redundant data that we are sending in the headers over and over, and sometimes there might be cookies increasing the headers size which results in bandwidth usage and increased latency. To overcome this, `HTTP/2` introduced header compression.
|
||||
|
||||

|
||||

|
||||
|
||||
Unlike request and response, headers are not compressed in `gzip` or `compress` etc formats but there is a different mechanism in place for header compression which is literal values are encoded using Huffman code and a headers table is maintained by the client and server and both the client and server omit any repetitive headers (e.g. user agent etc) in the subsequent requests and reference them using the headers table maintained by both.
|
||||
|
||||
@@ -210,8 +210,8 @@ Without any priority information, server processes the requests asynchronously i
|
||||
|
||||
There was extensive discussion on whether security (through `TLS`) should be made mandatory for `HTTP/2` or not. In the end, it was decided not to make it mandatory. However, most vendors stated that they will only support `HTTP/2` when it is used over `TLS`. So, although `HTTP/2` doesn't require encryption by specs but it has kind of become mandatory by default anyway. With that out of the way, `HTTP/2` when implemented over `TLS` does impose some requirements i.e. `TLS` version `1.2` or higher must be used, there must be a certain level of minimum key sizes, ephemeral keys are required etc.
|
||||
|
||||
`HTTP/2` is here and it has already [surpassed SPDY in adaption](http://caniuse.com/#search=http2) which is gradually increasing. `HTTP/2` has alot to offer in terms of performance gain and it is about time we should start using it.
|
||||
`HTTP/2` is here and it has already [surpassed SPDY in adaption](https://caniuse.com/#search=http2) which is gradually increasing. `HTTP/2` has alot to offer in terms of performance gain and it is about time we should start using it.
|
||||
|
||||
For anyone interested in further details here is the [link to specs](https:/http2.github.iohttp2-spec) and a link [demonstrating the performance benefits of `HTTP/2`](http://www.http2demo.io/).
|
||||
For anyone interested in further details here is the [link to specs](https:/http2.github.iohttp2-spec) and a link [demonstrating the performance benefits of `HTTP/2`](https://www.http2demo.io/).
|
||||
|
||||
And that about wraps it up. Until next time! stay tuned.
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/jwt-authentication.png)
|
||||
[](/guides/jwt-authentication.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/oauth.png)
|
||||
[](/guides/oauth.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
Random numbers are everywhere from computer games to lottery systems, graphics software, statistical sampling, computer simulation and cryptography. Graphic below is a quick explanation to how the random numbers are generated and why they may not be truly random.
|
||||
|
||||
[](/guides/random-numbers.png)
|
||||
[](/guides/random-numbers.png)
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ tags:
|
||||
|
||||
The chart below aims to give you a really basic understanding of how the capability of a DBMS is increased to handle a growing amount of load.
|
||||
|
||||
[](/guides/scaling-databases.svg)
|
||||
[](/guides/scaling-databases.svg)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/session-authentication.png)
|
||||
[](/guides/session-authentication.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/ssl-tls-https-ssh.png)
|
||||
[](/guides/ssl-tls-https-ssh.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/sso.png)
|
||||
[](/guides/sso.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/token-authentication.png)
|
||||
[](/guides/token-authentication.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/unfamiliar-codebase.png)
|
||||
[](/guides/unfamiliar-codebase.png)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/web-vitals.png)
|
||||
[](/guides/web-vitals.png)
|
||||
|
||||
|
||||
@@ -81,4 +81,4 @@ Cybersecurity refers to the protective measures against criminal activity accomp
|
||||
|
||||
<iframe class="w-full aspect-video mb-5" width="100%" height="400" src="https://www.youtube.com/embed/AuYNXgO_f3Y" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
And that wraps it up for this article. To learn more about the Internet, [Kamran Ahmed](https://twitter.com/kamranahmedse) has a nice little guide on [DNS: How a website is found on the Internet](/guides/dns-in-one-picture). Also, go through the episodes of [howdns.works](https://howdns.works/) and read this [cartoon intro to DNS over HTTPS](https://hacks.mozilla.org/2018/05/a-cartoon-intro-to-dns-over-https/).
|
||||
And that wraps it up for this article. To learn more about the Internet, [Kamran Ahmed](https://twitter.com/kamranahmedse) has a nice little guide on [DNS: How a website is found on the Internet](/guides/dns-in-one-picture/). Also, go through the episodes of [howdns.works](https://howdns.works/) and read this [cartoon intro to DNS over HTTPS](https://hacks.mozilla.org/2018/05/a-cartoon-intro-to-dns-over-https/).
|
||||
|
||||
@@ -20,5 +20,5 @@ tags:
|
||||
- "guide-sitemap"
|
||||
---
|
||||
|
||||
[](/guides/sli-slo-sla.jpeg)
|
||||
[](/guides/sli-slo-sla.jpeg)
|
||||
|
||||
|
||||
4
src/icons/down.svg
Normal file
4
src/icons/down.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m16.843 10.211c.108-.141.157-.3.157-.456 0-.389-.306-.755-.749-.755h-8.501c-.445 0-.75.367-.75.755 0 .157.05.316.159.457 1.203 1.554 3.252 4.199 4.258 5.498.142.184.36.29.592.29.23 0 .449-.107.591-.291 1.002-1.299 3.044-3.945 4.243-5.498z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -14,6 +14,7 @@ export interface Props {
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
noIndex?: boolean;
|
||||
permalink?: string;
|
||||
sponsor?: SponsorType;
|
||||
}
|
||||
|
||||
@@ -22,8 +23,16 @@ const {
|
||||
description = siteConfig.description,
|
||||
keywords = siteConfig.keywords,
|
||||
noIndex = false,
|
||||
permalink = '',
|
||||
sponsor,
|
||||
} = Astro.props;
|
||||
|
||||
// Remove trailing slashes to consider the page as canonical
|
||||
const currentPageAbsoluteUrl = `https://roadmap.sh${permalink}`;
|
||||
|
||||
const commitUrl = `https://github.com/kamranahmedse/developer-roadmap/commit/${
|
||||
import.meta.env.GITHUB_SHA
|
||||
}`;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -31,6 +40,7 @@ const {
|
||||
<head>
|
||||
<meta charset='UTF-8' />
|
||||
<meta name='generator' content={Astro.generator} />
|
||||
<meta name='commit' content={commitUrl} />
|
||||
<title>{title}</title>
|
||||
<meta name='description' content={description} />
|
||||
<meta name='author' content='Kamran Ahmed' />
|
||||
@@ -53,7 +63,9 @@ const {
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:type' content='website' />
|
||||
<meta property='og:url' content='https://roadmap.sh' />
|
||||
<meta property='og:url' content={currentPageAbsoluteUrl} />
|
||||
|
||||
<link rel='canonical' href={currentPageAbsoluteUrl} />
|
||||
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
|
||||
@@ -2,6 +2,7 @@ const formatter = Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
});
|
||||
|
||||
const defaultStarCount = 224000;
|
||||
let starCount: number | undefined = undefined;
|
||||
|
||||
export async function countStars(
|
||||
@@ -15,10 +16,10 @@ export async function countStars(
|
||||
const repoData = await fetch(`https://api.github.com/repos/${repo}`);
|
||||
const json = await repoData.json();
|
||||
|
||||
starCount = json.stargazers_count * 1;
|
||||
starCount = json.stargazers_count * 1 || defaultStarCount;
|
||||
} catch (e) {
|
||||
console.log('Failed to fetch stars', e);
|
||||
starCount = 224000;
|
||||
starCount = defaultStarCount;
|
||||
}
|
||||
|
||||
return starCount;
|
||||
@@ -27,11 +28,7 @@ export async function countStars(
|
||||
export async function getFormattedStars(
|
||||
repo = 'kamranahmedse/developer-roadmap'
|
||||
): Promise<string> {
|
||||
if (import.meta.env.DEV) {
|
||||
return '224k';
|
||||
}
|
||||
|
||||
const stars = await countStars(repo);
|
||||
const stars = import.meta.env.DEV ? defaultStarCount : await countStars(repo);
|
||||
|
||||
return formatter.format(stars);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Icon from '../components/Icon.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title='Page not found'>
|
||||
<BaseLayout title='Page not found' permalink={'/404/'}>
|
||||
<div class='bg-gray-100'>
|
||||
<div
|
||||
class='py-10 md:py-32 container flex flex-col md:flex-row items-center justify-center gap-7 '
|
||||
|
||||
@@ -15,16 +15,24 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { topicId } = Astro.params;
|
||||
const { file, breadcrumbs, roadmapId, roadmap, heading } = Astro.props as TopicFileType;
|
||||
const { file, breadcrumbs, roadmapId, roadmap, heading } =
|
||||
Astro.props as TopicFileType;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${heading} - roadmap.sh`} description={`Free resources to learn ${heading} in ${roadmap.featuredTitle}. Everything you need to know about ${heading} and how it realtes to ${roadmap.featuredTitle}.`} noIndex={true}>
|
||||
<BaseLayout
|
||||
title={`${heading} - roadmap.sh`}
|
||||
description={`Free resources to learn ${heading} in ${roadmap.featuredTitle}. Everything you need to know about ${heading} and how it realtes to ${roadmap.featuredTitle}.`}
|
||||
noIndex={true}
|
||||
permalink={`/${topicId}/`}
|
||||
>
|
||||
<RoadmapBanner roadmapId={roadmapId} roadmap={roadmap} />
|
||||
<div class="bg-gray-50">
|
||||
<div class='bg-gray-50'>
|
||||
<Breadcrumbs breadcrumbs={breadcrumbs} roadmapId={roadmapId} />
|
||||
|
||||
<div class="container pb-16 prose prose-p:mt-0 prose-h1:mb-4 prose-h2:mb-3 prose-h2:mt-0">
|
||||
<main id="main-content">
|
||||
<div
|
||||
class='container pb-16 prose prose-p:mt-0 prose-h1:mb-4 prose-h2:mb-3 prose-h2:mt-0'
|
||||
>
|
||||
<main id='main-content'>
|
||||
<file.Content />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import CaptchaScripts from '../../components/Captcha/CaptchaScripts.astro';
|
||||
import FAQs from '../../components/FAQs.astro';
|
||||
import InteractiveRoadmap from '../../components/InteractiveRoadmap/InteractiveRoadmap.astro';
|
||||
import MarkdownRoadmap from '../../components/MarkdownRoadmap.astro';
|
||||
import RoadmapHeader from '../../components/RoadmapHeader.astro';
|
||||
@@ -21,10 +22,12 @@ interface Params extends Record<string, string | undefined> {
|
||||
|
||||
const { roadmapId } = Astro.params as Params;
|
||||
const roadmapFile = await import(`../../roadmaps/${roadmapId}/${roadmapId}.md`);
|
||||
const questions = await import (`../../roadmaps/${roadmapId}/faqs.astro`);
|
||||
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
permalink={`/${roadmapId}/`}
|
||||
title={roadmapData?.seo?.title}
|
||||
description={roadmapData.seo.description}
|
||||
keywords={roadmapData.seo.keywords}
|
||||
@@ -52,7 +55,10 @@ const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
|
||||
{
|
||||
!roadmapData.isUpcoming && !roadmapData.jsonUrl && (
|
||||
<MarkdownRoadmap>
|
||||
<MarkdownRoadmap
|
||||
roadmapId={roadmapId}
|
||||
description={roadmapData.description}
|
||||
>
|
||||
<roadmapFile.Content />
|
||||
</MarkdownRoadmap>
|
||||
)
|
||||
@@ -60,5 +66,6 @@ const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
|
||||
{roadmapData.isUpcoming && <UpcomingRoadmap />}
|
||||
|
||||
<questions.default />
|
||||
<CaptchaScripts slot='after-footer' />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -22,7 +22,12 @@ const roadmapFile = await import(`../../roadmaps/${roadmapId}/${roadmapId}.md`);
|
||||
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${roadmapData.title} Topics`} description={roadmapData.seo.description} keywords={roadmapData.seo.keywords}>
|
||||
<BaseLayout
|
||||
title={`${roadmapData.title} Topics`}
|
||||
description={roadmapData.seo.description}
|
||||
keywords={roadmapData.seo.keywords}
|
||||
permalink={`/${roadmapId}/topics/`}
|
||||
>
|
||||
<RoadmapHeader
|
||||
description={roadmapData.description}
|
||||
title={`${roadmapData.featuredTitle} Topics`}
|
||||
@@ -31,8 +36,8 @@ const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
|
||||
hasTopics={false}
|
||||
/>
|
||||
|
||||
<div class="bg-gray-50 pt-5 pb-8 sm:pt-10 sm:pb-16">
|
||||
<div class="container">
|
||||
<div class='bg-gray-50 pt-5 pb-8 sm:pt-10 sm:pb-16'>
|
||||
<div class='container'>
|
||||
{
|
||||
topics.map((topic) => {
|
||||
// Breadcrumbs have three additional items e.g.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title='About roadmap.sh'>
|
||||
<BaseLayout title='About roadmap.sh' permalink={'/about/'}>
|
||||
<div class='bg-white border-b pt-7 pb-7 sm:pt-12 sm:pb-10'>
|
||||
<div class='container'>
|
||||
<div class='flex items-center'>
|
||||
|
||||
@@ -22,7 +22,11 @@ const { guide } = Astro.props;
|
||||
const { frontmatter: guideData } = guide;
|
||||
---
|
||||
|
||||
<BaseLayout title={guideData.seo.title} description={guideData.seo.description}>
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
permalink={`/guides/${guideId}/`}
|
||||
>
|
||||
<GuideHeader guide={guide} />
|
||||
|
||||
<div class='bg-gray-50 py-5 sm:py-10'>
|
||||
|
||||
@@ -7,7 +7,11 @@ import { getAllGuides } from '../../lib/guide';
|
||||
const guides = await getAllGuides();
|
||||
---
|
||||
|
||||
<BaseLayout title='Guides - roadmap.sh' description={'Detailed guides on Software Engineering Topics'}>
|
||||
<BaseLayout
|
||||
title='Guides - roadmap.sh'
|
||||
description={'Detailed guides on Software Engineering Topics'}
|
||||
permalink={`/guides/`}
|
||||
>
|
||||
<SimplePageHeader
|
||||
title='Guides'
|
||||
description='Succinct graphical explanations to engineering topics.'
|
||||
|
||||
@@ -13,7 +13,7 @@ const guides = await getAllGuides();
|
||||
const videos = await getAllVideos();
|
||||
---
|
||||
|
||||
<BaseLayout title='Developer Roadmaps' description={"Community driven roadmaps, articles and guides for developers to grow in their career."}>
|
||||
<BaseLayout title='Developer Roadmaps - roadmap.sh' description={"Community driven roadmaps, articles and guides for developers to grow in their career."} permalink={'/'}>
|
||||
<div class='bg-gradient-to-b from-slate-900 to-black'>
|
||||
<div class='border-b border-b-slate-900'>
|
||||
<div
|
||||
|
||||
@@ -15,6 +15,7 @@ Here is the list of PDF links for each of the roadmaps.
|
||||
* **QA Roadmap** - [Roadmap Link](https://roadmap.sh/qa) / [PDF Link](https://roadmap.sh/pdfs/qa.pdf)
|
||||
* **ASP.NET Core Roadmap** - [Roadmap Link](https://roadmap.sh/aspnet-core) / [PDF Link](https://roadmap.sh/pdfs/aspnet-core.pdf)
|
||||
* **Flutter Roadmap** - [Roadmap Link](https://roadmap.sh/flutter) / [PDF Link](https://roadmap.sh/pdfs/flutter.pdf)
|
||||
* **Go Roadmap** - [Roadmap Link](https://roadmap.sh/golang) / [PDF Link](https://roadmap.sh/pdfs/golang.pdf)
|
||||
* **Software Architect Roadmap** - [Roadmap Link](https://roadmap.sh/software-architect) / [PDF Link](https://roadmap.sh/pdfs/software-architect.pdf)
|
||||
* **Software Design and Architecture Roadmap** - [Roadmap Link](https://roadmap.sh/software-design-architecture) / [PDF Link](https://roadmap.sh/pdfs/software-design-architecture.pdf)
|
||||
* **JavaScript Roadmap** - [Roadmap Link](https://roadmap.sh/javascript) / [PDF Link](https://roadmap.sh/pdfs/javascript.pdf)
|
||||
@@ -25,6 +26,6 @@ Here is the list of PDF links for each of the roadmaps.
|
||||
* **Vue Roadmap** - [Roadmap Link](https://roadmap.sh/vue) / [PDF Link](https://roadmap.sh/pdfs/vue.pdf)
|
||||
* **Design System Roadmap** - [Roadmap Link](https://roadmap.sh/design-system) / [PDF Link](https://roadmap.sh/pdfs/design-system.pdf)
|
||||
* **Blockchain Roadmap** - [Roadmap Link](https://roadmap.sh/blockchain) / [PDF Link](https://roadmap.sh/pdfs/blockchain.pdf)
|
||||
* **Go Roadmap** - [Roadmap Link](https://roadmap.sh/golang) / [PDF Link](https://roadmap.sh/pdfs/go.pdf)
|
||||
* **Java Roadmap** - [Roadmap Link](https://roadmap.sh/java) / [PDF Link](https://roadmap.sh/pdfs/java.pdf)
|
||||
* **Spring Boot Roadmap** - [Roadmap Link](https://roadmap.sh/spring-boot) / [PDF Link](https://roadmap.sh/pdfs/spring-boot.pdf)
|
||||
* **Python Roadmap** - [Roadmap Link](https://roadmap.sh/python) / [PDF Link](https://roadmap.sh/pdfs/python.pdf)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
layout: ../layouts/MarkdownLayout.astro
|
||||
title: Privacy Policy - roadmap.sh
|
||||
noIndex: true
|
||||
---
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
By using or accessing the Services in any manner, you acknowledge that you accept the practices and policies outlined in this Privacy Policy, and you hereby consent that we will collect, use, and share your information in the following ways. Remember that your use of roadmap.sh’s Services is at all times subject to the [Terms of Use](/terms), which incorporates this Privacy Policy. Any terms we use in this Policy without defining them have the definitions given to them in the Terms of Use.
|
||||
By using or accessing the Services in any manner, you acknowledge that you accept the practices and policies outlined in this Privacy Policy, and you hereby consent that we will collect, use, and share your information in the following ways. Remember that your use of roadmap.sh’s Services is at all times subject to the [Terms of Use](/terms/), which incorporates this Privacy Policy. Any terms we use in this Policy without defining them have the definitions given to them in the Terms of Use.
|
||||
|
||||
## What does this Privacy Policy cover?
|
||||
|
||||
|
||||
@@ -8,16 +8,20 @@ const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
|
||||
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
|
||||
---
|
||||
|
||||
<BaseLayout title="Developer Roadmaps" description={"Step by step guides and paths to learn different tools or technologies"}>
|
||||
<BaseLayout
|
||||
title='Developer Roadmaps'
|
||||
description={'Step by step guides and paths to learn different tools or technologies'}
|
||||
permalink={'/roadmaps/'}
|
||||
>
|
||||
<SimplePageHeader
|
||||
title="Developer Roadmaps"
|
||||
description="Step by step guides and paths to learn different tools or technologies"
|
||||
title='Developer Roadmaps'
|
||||
description='Step by step guides and paths to learn different tools or technologies'
|
||||
showYouTubeAlert={true}
|
||||
/>
|
||||
|
||||
<div class="bg-gray-100 pt-4 pb-14 sm:pt-8 sm:pb-16">
|
||||
<div class="container">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-0.5 sm:gap-3">
|
||||
<div class='bg-gray-100 pt-4 pb-14 sm:pt-8 sm:pb-16'>
|
||||
<div class='container'>
|
||||
<div class='grid grid-cols-1 sm:grid-cols-2 gap-0.5 sm:gap-3'>
|
||||
{
|
||||
roleRoadmaps.map((roleRoadmap) => (
|
||||
<GridRoadmapItem roadmap={roleRoadmap} />
|
||||
|
||||
@@ -7,6 +7,8 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
<BaseLayout
|
||||
title='Signup - roadmap.sh'
|
||||
description='Register yourself to receive occasional emails about new roadmaps, updates, guides and videos'
|
||||
permalink={'/signup/'}
|
||||
noIndex={true}
|
||||
>
|
||||
<div class='container'>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: ../layouts/MarkdownLayout.astro
|
||||
title: Terms and Conditions - roadmap.sh
|
||||
noIndex: true
|
||||
---
|
||||
|
||||
# Terms of Service
|
||||
@@ -21,7 +22,7 @@ Except for changes by us as described here, no other amendment or modification o
|
||||
|
||||
## Do these terms cover privacy?
|
||||
|
||||
You can view the current roadmap.sh [Privacy Policy here](/privacy).
|
||||
You can view the current roadmap.sh [Privacy Policy here](/privacy/).
|
||||
|
||||
The Children’s Online Privacy Protection Act (“COPPA”) requires that online service providers obtain parental consent before they knowingly collect personally identifiable information online from children who are under 13. We do not knowingly collect or solicit personally identifiable information from children under 13. If you are a child under 13, please do not attempt to register for the Services or send any personal information about yourself to us. If we learn we have collected personal information from a child under 13, we will delete that information as quickly as possible. If you believe that a child under 13 may have provided us personal information, please contact us at kamranahmed.se@gmail.com.
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const { video } = Astro.props;
|
||||
<BaseLayout
|
||||
title={video.frontmatter.title}
|
||||
description={video.frontmatter.description}
|
||||
permalink={`/videos/${videoId}/`}
|
||||
>
|
||||
<VideoHeader video={video} />
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const videos = await getAllVideos();
|
||||
<BaseLayout
|
||||
title='Illustrated Videos - roadmap.sh'
|
||||
description={'Graphical video demonstrations on software engineering topics.'}
|
||||
permalink={`/videos/`}
|
||||
>
|
||||
<SimplePageHeader
|
||||
title='Videos'
|
||||
|
||||
@@ -119,7 +119,7 @@ I would highly recommend watching [this free course](https://www.udacity.com/cou
|
||||
* [Developing Android Apps with Kotlin](https://www.udacity.com/course/developing-android-apps-with-kotlin--ud9012)
|
||||
* [Android Basics in Kotlin](https://developer.android.com/courses/android-basics-kotlin/course)
|
||||
* [Android Developer Guides](https://developer.android.com/guide)
|
||||
* [Raywenderlich](https://www.raywenderlich.com)
|
||||
* [Kodeco](https://www.kodeco.com)
|
||||
|
||||
## Wrap Up
|
||||
|
||||
|
||||
0
src/roadmaps/android/faqs.astro
Normal file
0
src/roadmaps/android/faqs.astro
Normal file
@@ -4,5 +4,5 @@ A schematic is a template-based code generator that supports complex logic. It i
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Angular Website](https://angular.io/guide/schematics#:~:text=A%20schematic%20is%20a%20template,collections%20and%20installed%20with%20npm.)
|
||||
- [Angular Website](https://angular.io/guide/schematics)
|
||||
- [Angular Blog](https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2?gi=ad9571373944)
|
||||
|
||||
@@ -4,4 +4,4 @@ Angular processes all data bindings once for each JavaScript event cycle, from t
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Angular.io Website](https://angular.io/guide/architecture-components#:~:text=Angular%20processes%20all%20data%20bindings,between%20parent%20and%20child%20components.)
|
||||
- [Angular.io Website](https://angular.io/guide/architecture-components)
|
||||
|
||||
0
src/roadmaps/angular/faqs.astro
Normal file
0
src/roadmaps/angular/faqs.astro
Normal file
0
src/roadmaps/aspnet-core/faqs.astro
Normal file
0
src/roadmaps/aspnet-core/faqs.astro
Normal file
@@ -11,7 +11,7 @@ dimensions:
|
||||
width: 968
|
||||
height: 2840.4
|
||||
seo:
|
||||
title: "Backend Developer Roadmap: Learn to become a modern backend developer"
|
||||
title: "Backend Developer Roadmap"
|
||||
description: "Learn to become a modern backend developer using this roadmap. Community driven, articles, resources, guides, interview questions, quizzes for modern backend development."
|
||||
keywords:
|
||||
- "backend roadmap 2023"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Git
|
||||
|
||||
[Git](https://git-scm.com/) is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [Learn Git on the command line](https://github.com/jlord/git-it-electron)
|
||||
- [Version Control System Introduction](https://www.youtube.com/watch?v=zbKdDsNNOhg)
|
||||
- [Git & GitHub Crash Course For Beginners](https://www.youtube.com/watch?v=SWYqp7iY_Tc)
|
||||
- [Learn Git in 20 Minutes](https://youtu.be/Y9XZQO1n_7c?t=21)
|
||||
@@ -10,5 +10,4 @@ Service meshes are commonly used in cloud-native architectures and are often man
|
||||
|
||||
Visit the following resources to learn more:
|
||||
|
||||
- [What is a Service Mesh?](https://www.nginx.com/blog/what-is-a-service-mesh/)
|
||||
- [Service Mesh Explained - Cloud Native Computing Foundation (CNCF)](https://www.cncf.io/blog/2018/05/02/service-mesh-explained/)
|
||||
- [What is a Service Mesh?](https://www.nginx.com/blog/what-is-a-service-mesh/)
|
||||
140
src/roadmaps/backend/faqs.astro
Normal file
140
src/roadmaps/backend/faqs.astro
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
import Answer from '../../components/FAQs/Answer.astro';
|
||||
import FAQs from '../../components/FAQs/FAQs.astro';
|
||||
import Question from '../../components/FAQs/Question.astro';
|
||||
|
||||
const salaryLink =
|
||||
'https://www.glassdoor.com/Salaries/united-states-backend-developer-salary-SRCH_IL.0,13_IN1_KO14,31.htm';
|
||||
---
|
||||
|
||||
<FAQs>
|
||||
<Question isActive question='What is Backend Development?'>
|
||||
<Answer>
|
||||
<p class='mb-3'>
|
||||
Backend web development is the part of web development that deals with
|
||||
the server-side of a web application. This includes creating and
|
||||
managing the server-side logic, connecting the application to a
|
||||
database, creating server-side APIs, handling user authentication and
|
||||
authorization, and processing and responding to user requests. It often
|
||||
involves the use of programming languages such as Python, Java, Ruby,
|
||||
PHP, JavaScript (Node.js), and .NET languages.
|
||||
</p>
|
||||
<p>
|
||||
A backend developer is responsible for the development of server-side
|
||||
components of a web application i.e. working with databases, handling
|
||||
requests, creating server-side APIs that can be consumed by frontend
|
||||
developers to retrieve and manipulate data, ensuring the scalability of
|
||||
the systems i.e. making sure that the backend can handle a high volume
|
||||
of traffic and is performant, integrating external services like payment
|
||||
gateways, message queues, cloud services, etc.
|
||||
</p>
|
||||
</Answer>
|
||||
</Question>
|
||||
|
||||
<Question question='How to become a Backend Developer?'>
|
||||
<Answer>
|
||||
<p class='mb-3'>
|
||||
If you are a complete beginner who is just getting started, you can
|
||||
start by learning <span class='bg-yellow-100'
|
||||
>a backend programming language</span
|
||||
> such as Python, Ruby, Java, Go etc. Once you have got the basic to intermediate
|
||||
understanding of the language, learn about <span class='bg-yellow-100'
|
||||
>the package manager</span
|
||||
> for that language and learn how to install and use external packages into
|
||||
your applications. Learn the basics of some <span class='bg-yellow-100'
|
||||
>relational database</span
|
||||
> e.g. PostgreSQL and learn how to run simple CRUD operations. Optionally,
|
||||
you can pick up and learn a <span class='bg-yellow-100'
|
||||
>web framework</span
|
||||
> for the language of your choice as well. Learn how to build a <span
|
||||
class='bg-yellow-100'>simple RESTful API</span
|
||||
> and implement simple Authentication/Authorization into it. While you are
|
||||
learning all the items mentioned above, don't forget to learn about <span
|
||||
class='bg-yellow-100'>Git and GitHub</span
|
||||
> as well.
|
||||
</p>
|
||||
|
||||
<p class='mb-3'>
|
||||
After following all the instructions above, you can start applying for
|
||||
the entry level backend developer jobs. Also, look at the backend
|
||||
developer roadmap above to get an idea about the landscape and see what
|
||||
else you are missing. A degree in computer science or related field is
|
||||
not always necessary but networking, building a portfolio and actively
|
||||
seeking internships, junior developer positions or consulting can help
|
||||
to start and advance a career as a backend developer.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note: remember to make a lot of projects while you are learning to
|
||||
solidify your understanding of the concepts. Also, it's important to
|
||||
have the attitude of continuous learning to improve your skills and be
|
||||
prepared for the fast-paced technology evolution in the industry.
|
||||
</p>
|
||||
</Answer>
|
||||
</Question>
|
||||
<Question question='How long does it take to become a Backend Developer?'>
|
||||
<Answer>
|
||||
<p class='mb-3'>
|
||||
The amount of time it takes to become a backend developer can vary
|
||||
depending on several factors, such as your learning pace, previous
|
||||
experience and the amount of time you are able to dedicate to learning.
|
||||
</p>
|
||||
<p>
|
||||
If you have a background in computer science or a related field, and
|
||||
have experience with programming, you may be able to become a backend
|
||||
developer relatively quickly, potentially within a few months. However,
|
||||
if you are starting with little or no prior experience or education in
|
||||
computer science, it may take longer to develop the necessary skills and
|
||||
gain the experience needed to be a proficient backend developer. It
|
||||
could take anywhere from 6 months to 2 years.
|
||||
</p>
|
||||
</Answer>
|
||||
</Question>
|
||||
<Question question='What are the Backend Developer salaries?'>
|
||||
<Answer>
|
||||
<p class='mb-3'>
|
||||
Backend developer salaries can vary depending on factors such as
|
||||
location, experience, and company size. According to data from
|
||||
Glassdoor, the average base salary for a backend developer in the United
|
||||
States is around $92,000 per year. However, this number can vary greatly
|
||||
depending on location, with the highest-paying cities such as San
|
||||
Francisco, Seattle and New York having an average salary of $120,000 to
|
||||
$135,000 per year.
|
||||
</p>
|
||||
|
||||
<p class='mb-3'>
|
||||
It's important to keep in mind that these are just averages, and
|
||||
salaries can vary greatly depending on factors such as experience level,
|
||||
specific skills, and the company you work for. With more experience and
|
||||
specific skills relevant to the job you are applying for you can expect
|
||||
to earn more.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It is worth looking at a range of resources, including salary surveys,
|
||||
and job boards to get a general understanding of the current market in
|
||||
your location and experience level. Also try reaching out to other
|
||||
professionals in the field and getting an understanding of their own
|
||||
experience and salary ranges.
|
||||
</p>
|
||||
</Answer>
|
||||
</Question>
|
||||
|
||||
<Question question='Should I learn everything listed on the Backend Roadmap?'>
|
||||
<Answer>
|
||||
<p class='mb-3'>
|
||||
This roadmap contains everything that you might encounter while working
|
||||
as a Backend Developer. You may not need everything listed on this
|
||||
roadmap to get into the industry; every job is different and most of the
|
||||
jobs will require a subset of the items on the roadmap. However, knowing
|
||||
what you don't know is as important as knowing things, so you can use
|
||||
this roadmap to get an idea of what you are missing as well.
|
||||
</p>
|
||||
<p>
|
||||
If you are a beginner who is just getting started, don't feel
|
||||
overwhelmed by looking at this roadmap. Look at the answer to the FAQ
|
||||
"How to become a Backend Developer?"
|
||||
</p>
|
||||
</Answer>
|
||||
</Question>
|
||||
</FAQs>
|
||||
0
src/roadmaps/blockchain/faqs.astro
Normal file
0
src/roadmaps/blockchain/faqs.astro
Normal file
@@ -1,4 +1,4 @@
|
||||
# gPRC
|
||||
# gRPC
|
||||
|
||||
gRPC is a platform agnostic serialization protocol that is used to communicate between services. Designed by Google in 2015, it is a modern alternative to REST APIs. It is a binary protocol that uses HTTP/2 as a transport layer. It is a high performance, open source, general-purpose RPC framework that puts mobile and HTTP/2 first.
|
||||
|
||||
|
||||
0
src/roadmaps/computer-science/faqs.astro
Normal file
0
src/roadmaps/computer-science/faqs.astro
Normal file
0
src/roadmaps/cyber-security/faqs.astro
Normal file
0
src/roadmaps/cyber-security/faqs.astro
Normal file
@@ -1,7 +1,7 @@
|
||||
---
|
||||
jsonUrl: "/jsons/design-system.json"
|
||||
pdfUrl: "/pdfs/design-system.pdf"
|
||||
order: 10
|
||||
order: 11
|
||||
featuredTitle: "Design System"
|
||||
featuredDescription: "Step by step guide to building a modern Design System"
|
||||
title: "Design System"
|
||||
|
||||
0
src/roadmaps/design-system/faqs.astro
Normal file
0
src/roadmaps/design-system/faqs.astro
Normal file
@@ -11,14 +11,14 @@ dimensions:
|
||||
width: 968
|
||||
height: 2527.46
|
||||
sponsor:
|
||||
url: "https://www.getambassador.io/edge-stack-guide-v4?utm_source=roadmap.sh&utm_medium=ebook&utm_campaign=edgestack-guide"
|
||||
url: "https://www.tigera.io/lp/kubernetes-security-and-observability-ebook/?utm_campaign=O%27Reilly-book-final-release&utm_medium=web&utm_source=Insight-Partners-popup"
|
||||
title: "Free eBook"
|
||||
imageUrl: "https://i.imgur.com/0bH1Vl6.png"
|
||||
description: "Learn about API Gateways, Microservices, Load Balancing, and more with this free eBook."
|
||||
imageUrl: "/images/devops-ebook.png"
|
||||
description: "Learn how to secure and troubleshoot your cloud-native applications with this free eBook."
|
||||
event:
|
||||
category: "SponsorClick"
|
||||
action: "Ambassador EBook Redirect"
|
||||
label: "Clicked Ambassador EBook Link"
|
||||
action: "Tigera EBook Redirect"
|
||||
label: "Clicked Tigera EBook Link"
|
||||
seo:
|
||||
title: "DevOps Roadmap: Learn to become a DevOps Engineer or SRE"
|
||||
description: "Community driven, articles, resources, guides, interview questions, quizzes for DevOps. Learn to become a modern DevOps engineer by following the steps, skills, resources and guides listed in this roadmap."
|
||||
|
||||
0
src/roadmaps/devops/faqs.astro
Normal file
0
src/roadmaps/devops/faqs.astro
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user