Compare commits
221 Commits
feat/types
...
fix/guide-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69e8e5e19b | ||
|
|
9893e9f0a3 | ||
|
|
caf1cd0269 | ||
|
|
d21c1f6d0d | ||
|
|
9d38cf7650 | ||
|
|
d232d3bbd0 | ||
|
|
366d893df9 | ||
|
|
ff27561765 | ||
|
|
133642e05f | ||
|
|
7434ff71eb | ||
|
|
d081ecf5b3 | ||
|
|
d8a039690b | ||
|
|
56f0df549d | ||
|
|
b042161e29 | ||
|
|
db273210fd | ||
|
|
9370e262c0 | ||
|
|
e0f9bc8456 | ||
|
|
af211ab129 | ||
|
|
eab1fb31b2 | ||
|
|
c36fd71ec1 | ||
|
|
7d0e35d7ae | ||
|
|
59d881a77b | ||
|
|
b57e3ecc75 | ||
|
|
cd5c0c10a2 | ||
|
|
10a5e4c0ae | ||
|
|
432983631d | ||
|
|
29189062b9 | ||
|
|
84138d5049 | ||
|
|
c28ac4b078 | ||
|
|
66bdbd7458 | ||
|
|
907f820778 | ||
|
|
ec0a8a99ef | ||
|
|
a7a342c8e7 | ||
|
|
e0e26580fa | ||
|
|
f9d96d415f | ||
|
|
e1c22932be | ||
|
|
9dae1b3595 | ||
|
|
cdb642c8d4 | ||
|
|
71b43af862 | ||
|
|
69dccb3fcc | ||
|
|
ca6ddb4654 | ||
|
|
0ac6fc70ff | ||
|
|
5ddb021898 | ||
|
|
d227603a59 | ||
|
|
e0d70950ac | ||
|
|
148bfd8736 | ||
|
|
648985cefc | ||
|
|
9321ac6aa1 | ||
|
|
170ab3a6cf | ||
|
|
708fa31998 | ||
|
|
f83a1a6c3b | ||
|
|
dc1d7ef226 | ||
|
|
808bd40cce | ||
|
|
45ce59b10d | ||
|
|
e3f41ec0e3 | ||
|
|
4f821d0f1d | ||
|
|
ec1283a5dc | ||
|
|
4da909d358 | ||
|
|
0beb9ad239 | ||
|
|
c6213dde92 | ||
|
|
3d655965f6 | ||
|
|
c5c2ee3b2c | ||
|
|
cad0813eb6 | ||
|
|
f9c1e6e0a2 | ||
|
|
d3578756d4 | ||
|
|
5fe506324a | ||
|
|
bc007dcc9b | ||
|
|
42a5d5bba6 | ||
|
|
f908c5371d | ||
|
|
78964b9f65 | ||
|
|
9f11de60ed | ||
|
|
4ba28b702b | ||
|
|
45c6fc873f | ||
|
|
23b7c21502 | ||
|
|
d474d07ebb | ||
|
|
7d08572d78 | ||
|
|
17fc85f893 | ||
|
|
5ccba8d7c0 | ||
|
|
fefbb4f833 | ||
|
|
bcbc9c9d54 | ||
|
|
73d1d0e389 | ||
|
|
14856560c3 | ||
|
|
b097395c07 | ||
|
|
929be729e5 | ||
|
|
1eb8fab15e | ||
|
|
51233c8011 | ||
|
|
0081e9059c | ||
|
|
4e66148777 | ||
|
|
18e430be0b | ||
|
|
e39d0d93e0 | ||
|
|
9260dc36b5 | ||
|
|
f5f846ed73 | ||
|
|
7518d60013 | ||
|
|
59d9674d75 | ||
|
|
16fb03086e | ||
|
|
c0f46c5eed | ||
|
|
b5b8b92791 | ||
|
|
8f90dac32e | ||
|
|
e9f3a616d1 | ||
|
|
c28ed87247 | ||
|
|
53fb6313db | ||
|
|
cc933b238d | ||
|
|
48fba932b4 | ||
|
|
9961259ffb | ||
|
|
cab2054c1d | ||
|
|
d34affb420 | ||
|
|
ee4f0980bc | ||
|
|
37fdd010a8 | ||
|
|
aa04c51a12 | ||
|
|
7993f12d12 | ||
|
|
1a3265295c | ||
|
|
238245431b | ||
|
|
48c04055d5 | ||
|
|
596b8f56ac | ||
|
|
45267693e2 | ||
|
|
f932df8627 | ||
|
|
8dcf4b00c4 | ||
|
|
cb32a9610d | ||
|
|
01c090f62d | ||
|
|
60b1edcab9 | ||
|
|
d08887060f | ||
|
|
24a6c4930e | ||
|
|
e57b889f73 | ||
|
|
c5d14d2543 | ||
|
|
4f0b08ea93 | ||
|
|
47e2dbdd12 | ||
|
|
f1ad70acd9 | ||
|
|
ac230bbf29 | ||
|
|
d0861711ac | ||
|
|
74b2dda7f7 | ||
|
|
2b49fa3182 | ||
|
|
e2a8240e35 | ||
|
|
a7f45c0af1 | ||
|
|
77a6270bd7 | ||
|
|
64d3ad662c | ||
|
|
c8b8e12b64 | ||
|
|
8f94a5887e | ||
|
|
00b6217e63 | ||
|
|
1a0d7463eb | ||
|
|
983ee44632 | ||
|
|
f393cb186e | ||
|
|
70edfb0ac2 | ||
|
|
ed07d34d64 | ||
|
|
831521ae10 | ||
|
|
e29289f0dc | ||
|
|
0fd3eb0cc6 | ||
|
|
f58a77010b | ||
|
|
6303e31c0e | ||
|
|
dfc2d39427 | ||
|
|
5e75026424 | ||
|
|
7a4c077a90 | ||
|
|
e45c49a404 | ||
|
|
b6a0255f12 | ||
|
|
b741a0e1ee | ||
|
|
8200993af4 | ||
|
|
5c1d803892 | ||
|
|
dcf0f94af9 | ||
|
|
4ad8886aa0 | ||
|
|
a1143cd6cb | ||
|
|
f130c706da | ||
|
|
8068face54 | ||
|
|
39866117a6 | ||
|
|
df7aa17f86 | ||
|
|
ee6572660b | ||
|
|
9875a2d6f7 | ||
|
|
5b180e2597 | ||
|
|
6f05972493 | ||
|
|
4bd182e4d0 | ||
|
|
68d319cacb | ||
|
|
3e76df8d2a | ||
|
|
9d69477947 | ||
|
|
e0ead47fb1 | ||
|
|
253c88542f | ||
|
|
5bca9834fb | ||
|
|
dde6e3d3df | ||
|
|
6fe8fee25f | ||
|
|
f3622a1b1c | ||
|
|
a92bda38f4 | ||
|
|
b194d167be | ||
|
|
ec04b582a6 | ||
|
|
f55159a12b | ||
|
|
938c7796d1 | ||
|
|
e04bd9db05 | ||
|
|
7c837d14da | ||
|
|
cc05587d9e | ||
|
|
2172014d6e | ||
|
|
98d43e76b7 | ||
|
|
7665970813 | ||
|
|
d96e5890b9 | ||
|
|
659bd93094 | ||
|
|
a4dddfb19b | ||
|
|
12a4be2227 | ||
|
|
9edcb35acb | ||
|
|
1df4e4b836 | ||
|
|
49e78cf1c0 | ||
|
|
a4a29b4efa | ||
|
|
3e49e7f91d | ||
|
|
7627bc73b5 | ||
|
|
26eaa40dc1 | ||
|
|
45a0b53d5f | ||
|
|
7bac3c3444 | ||
|
|
62905bda7a | ||
|
|
179bf366cc | ||
|
|
59d47c5b1e | ||
|
|
d23ea8e577 | ||
|
|
07f001f8be | ||
|
|
754a91acef | ||
|
|
16c550211b | ||
|
|
a56710c43d | ||
|
|
00f94e031e | ||
|
|
d1556c85df | ||
|
|
1885d6d304 | ||
|
|
3b8c8316b3 | ||
|
|
034fd16a1f | ||
|
|
aa9bf2f263 | ||
|
|
6a5df98f4f | ||
|
|
ea02c8835a | ||
|
|
e13733a503 | ||
|
|
6f0ad58764 | ||
|
|
f68c303ffa | ||
|
|
b2c79ff395 |
43
.github/workflows/update-sponsors.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Update Sponsors
|
||||
|
||||
on:
|
||||
workflow_dispatch: # allow manual run
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # run daily at 00:00 UTC
|
||||
|
||||
env:
|
||||
SPONSOR_SHEET_API_KEY: ${{ secrets.SPONSOR_SHEET_API_KEY }}
|
||||
SPONSOR_SHEET_ID: ${{ secrets.SPONSOR_SHEET_ID }}
|
||||
|
||||
jobs:
|
||||
update-sponsors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: pnpm/action-setup@v2.2.2
|
||||
with:
|
||||
version: 7.13.4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
- name: Update sponsors
|
||||
run: |
|
||||
node bin/update-sponsors.cjs
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
delete-branch: false
|
||||
branch: 'update-sponsors'
|
||||
base: 'master'
|
||||
labels: |
|
||||
sponsors
|
||||
automated pr
|
||||
reviewers: kamranahmedse
|
||||
commit-message: 'chore: update sponsors'
|
||||
title: 'Update Sponsor Banners'
|
||||
body: |
|
||||
Updates sponsor banners.
|
||||
Please review the changes and merge if everything looks good.
|
||||
7
.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
app-dist
|
||||
dist
|
||||
.idea
|
||||
.github
|
||||
public
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
18
.prettierrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
overrides: [
|
||||
{
|
||||
files: '*.astro',
|
||||
options: {
|
||||
parser: 'astro',
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('prettier-plugin-astro'),
|
||||
require('prettier-plugin-tailwindcss'),
|
||||
],
|
||||
};
|
||||
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"prettier.documentSelectors": ["**/*.astro"],
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import rehypeExternalLinks from 'rehype-external-links';
|
||||
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://roadmap.sh',
|
||||
site: 'https://roadmap.sh/',
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: 'dracula',
|
||||
@@ -17,6 +17,24 @@ export default defineConfig({
|
||||
rehypeExternalLinks,
|
||||
{
|
||||
target: '_blank',
|
||||
rel: function (element) {
|
||||
const href = element.properties.href;
|
||||
const whiteListedStarts = [
|
||||
'/',
|
||||
'#',
|
||||
'mailto:',
|
||||
'https://github.com/kamranahmedse',
|
||||
'https://thenewstack.io',
|
||||
'https://cs.fyi',
|
||||
'https://roadmap.sh',
|
||||
];
|
||||
|
||||
if (whiteListedStarts.some((start) => href.startsWith(start))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return 'noopener noreferrer nofollow';
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
@@ -3,7 +3,10 @@ const path = require('path');
|
||||
|
||||
const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the best-practices
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(__dirname, '../src/best-practices');
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(
|
||||
__dirname,
|
||||
'../src/data/best-practices'
|
||||
);
|
||||
const bestPracticeId = process.argv[2];
|
||||
|
||||
const allowedBestPracticeId = fs.readdirSync(BEST_PRACTICE_CONTENT_DIR);
|
||||
@@ -28,7 +31,10 @@ if (!bestPracticeDirName) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bestPracticeDirPath = path.join(BEST_PRACTICE_CONTENT_DIR, bestPracticeDirName);
|
||||
const bestPracticeDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName
|
||||
);
|
||||
const bestPracticeContentDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName,
|
||||
@@ -37,7 +43,9 @@ const bestPracticeContentDirPath = path.join(
|
||||
|
||||
// If best practice content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(bestPracticeContentDirPath)) {
|
||||
console.error(`Best Practice content already exists @ ${bestPracticeContentDirPath}`);
|
||||
console.error(
|
||||
`Best Practice content already exists @ ${bestPracticeContentDirPath}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -51,7 +59,11 @@ function prepareDirTree(control, dirTree) {
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || controlName.startsWith('check:') || controlName.startsWith('ext_link:')) {
|
||||
if (
|
||||
!controlName ||
|
||||
controlName.startsWith('check:') ||
|
||||
controlName.startsWith('ext_link:')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,7 +88,10 @@ function prepareDirTree(control, dirTree) {
|
||||
return { dirTree };
|
||||
}
|
||||
|
||||
const bestPractice = require(path.join(__dirname, `../public/jsons/best-practices/${bestPracticeId}`));
|
||||
const bestPractice = require(path.join(
|
||||
__dirname,
|
||||
`../public/jsons/best-practices/${bestPracticeId}`
|
||||
));
|
||||
const controls = bestPractice.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating
|
||||
@@ -129,11 +144,7 @@ function createDirTree(parentDir, dirTree, filePaths = {}) {
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
filePaths
|
||||
);
|
||||
createDirTree(path.join(parentDir, dirName), dirTree[dirName], filePaths);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
@@ -141,4 +152,4 @@ function createDirTree(parentDir, dirTree, filePaths = {}) {
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(bestPracticeContentDirPath, dirTree);
|
||||
console.log('Created best practice content directory structure');
|
||||
console.log('Created best practice content directory structure');
|
||||
@@ -1,4 +1,5 @@
|
||||
## CLI Tools
|
||||
|
||||
> A bunch of CLI scripts to make the development easier
|
||||
|
||||
## `roadmap-links.cjs`
|
||||
@@ -9,12 +10,22 @@ Generates a list of all the resources links in any roadmap file.
|
||||
|
||||
Compresses all the JSON files in the `public/jsons` folder
|
||||
|
||||
## `update-sponsors.cjs`
|
||||
|
||||
Updates the sponsor ads on each roadmap page with the latest sponsor information in the Excel sheet.
|
||||
|
||||
## `roadmap-content.cjs`
|
||||
|
||||
Currently, for any new roadmaps that we add, we do create the interactive roadmap but we end up leaving the content empty in the roadmap till we get time to fill it up manually.
|
||||
|
||||
This script populates all the content files with some minimal content from OpenAI so that the users visiting the website have something to read in the interactive roadmap till we get time to fill it up manually.
|
||||
|
||||
## `roadmap-dirs.cjs`
|
||||
|
||||
This command is used to create the content folders and files for the interactivity of the roadmap. You can use the below command to generate the roadmap skeletons inside a roadmap directory:
|
||||
|
||||
```bash
|
||||
npm run roadmap-content [frontend|backend|devops|...]
|
||||
npm run roadmap-dirs [frontend|backend|devops|...]
|
||||
```
|
||||
|
||||
For the content skeleton to be generated, we should have proper grouping, and the group names in the project files. You can follow the steps listed below in order to add the meta information to the roadmap.
|
||||
@@ -24,5 +35,3 @@ For the content skeleton to be generated, we should have proper grouping, and th
|
||||
- Assign the name to the groups.
|
||||
- Group names have the format of `[sort]-[slug]` e.g. `100-internet`. Each group name should start with a number starting from 100 which helps with sorting of the directories and the files. Groups at the same level have the sequential sorting information.
|
||||
- Each groups children have a separate group and have the name similar to `[sort]-[parent-slug]:[child-slug]` where sort refers to the sorting of the `child-slug` and not the parent. Also parent-slug does not need to have the sorting information as a part of slug e.g. if parent was `100-internet` the children would be `100-internet:how-does-the-internet-work`, `101-internet:what-is-http`, `102-internet:browsers`.
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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 OPEN_AI_API_KEY = process.env.OPEN_AI_API_KEY;
|
||||
const ALL_ROADMAPS_DIR = path.join(__dirname, '../src/data/roadmaps');
|
||||
const ROADMAP_JSON_DIR = path.join(__dirname, '../public/jsons/roadmaps');
|
||||
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
const allowedRoadmapIds = fs.readdirSync(ALL_ROADMAPS_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
@@ -18,146 +19,144 @@ if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the roadmap content files
|
||||
const roadmapDirName = fs
|
||||
.readdirSync(ROADMAP_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === roadmapId);
|
||||
|
||||
if (!roadmapDirName) {
|
||||
console.error('Roadmap directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDirPath = path.join(ROADMAP_CONTENT_DIR, roadmapDirName);
|
||||
const roadmapContentDirPath = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If roadmap content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(roadmapContentDirPath)) {
|
||||
console.error(`Roadmap content already exists @ ${roadmapContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
// e.g. 104
|
||||
const sortOrder = controlName.match(/^\d+/)?.[0];
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || !sortOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. testing-your-apps:other-options
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlNameWithoutSortOrder.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
dirSortOrders[controlNameWithoutSortOrder] = Number(sortOrder);
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree, dirSortOrders);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree, dirSortOrders };
|
||||
}
|
||||
|
||||
const roadmap = require(path.join(__dirname, `../public/jsons/roadmaps/${roadmapId}`));
|
||||
const controls = roadmap.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating and also calculate the sort orders
|
||||
const dirTree = {};
|
||||
const dirSortOrders = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree, dirSortOrders);
|
||||
const ROADMAP_CONTENT_DIR = path.join(ALL_ROADMAPS_DIR, roadmapId, 'content');
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
apiKey: OPEN_AI_API_KEY,
|
||||
});
|
||||
|
||||
/**
|
||||
* @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;
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
// @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+-/, ':');
|
||||
function getFilesInFolder(folderPath, fileList = {}) {
|
||||
const files = fs.readdirSync(folderPath);
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(folderPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
const sortOrder = sortOrders[groupName] || '';
|
||||
if (stats.isDirectory()) {
|
||||
getFilesInFolder(filePath, fileList);
|
||||
} else if (stats.isFile()) {
|
||||
const fileUrl = filePath
|
||||
.replace(ROADMAP_CONTENT_DIR, '') // Remove the content folder
|
||||
.replace(/\/\d+-/g, '/') // Remove ordering info `/101-ecosystem`
|
||||
.replace(/\/index\.md$/, '') // Make the `/index.md` to become the parent folder only
|
||||
.replace(/\.md$/, ''); // Remove `.md` from the end of file
|
||||
|
||||
// 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
|
||||
);
|
||||
fileList[fileUrl] = filePath;
|
||||
}
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
return fileList;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(roadmapContentDirPath, dirTree, dirSortOrders);
|
||||
console.log('Created roadmap content directory structure');
|
||||
function writeTopicContent(currTopicUrl) {
|
||||
const [parentTopic, childTopic] = currTopicUrl
|
||||
.replace(/^\d+-/g, '/')
|
||||
.replace(/:/g, '/')
|
||||
.replace(/^\//, '')
|
||||
.split('/')
|
||||
.slice(-2)
|
||||
.map((topic) => topic.replace(/-/g, ' '));
|
||||
|
||||
const roadmapTitle = roadmapId.replace(/-/g, ' ');
|
||||
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief summary for that topic. Content should be in markdown. Behave as if you are the author of the guide.`;
|
||||
if (!childTopic) {
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief summary for that topic. Content should be in markdown. Behave as if you are the author of the guide.`;
|
||||
}
|
||||
|
||||
console.log(`Genearting '${childTopic || parentTopic}'...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
openai
|
||||
.createChatCompletion({
|
||||
model: 'gpt-4',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.data.choices[0].message.content;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const topicUrlToPathMapping = getFilesInFolder(ROADMAP_CONTENT_DIR);
|
||||
|
||||
const roadmapJson = require(path.join(ROADMAP_JSON_DIR, `${roadmapId}.json`));
|
||||
const groups = roadmapJson?.mockup?.controls?.control?.filter(
|
||||
(control) =>
|
||||
control.typeID === '__group__' &&
|
||||
!control.properties?.controlName?.startsWith('ext_link')
|
||||
);
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
console.log('----------------------------------------');
|
||||
console.log('OPEN_AI_API_KEY not found. Skipping openai api calls...');
|
||||
console.log('----------------------------------------');
|
||||
}
|
||||
|
||||
for (let group of groups) {
|
||||
const topicId = group?.properties?.controlName;
|
||||
const topicTitle = group?.children?.controls?.control?.find(
|
||||
(control) => control?.typeID === 'Label'
|
||||
)?.properties?.text;
|
||||
const currTopicUrl = topicId?.replace(/^\d+-/g, '/')?.replace(/:/g, '/');
|
||||
if (!currTopicUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentFilePath = topicUrlToPathMapping[currTopicUrl];
|
||||
|
||||
if (!contentFilePath) {
|
||||
console.log(`Missing file for: ${currTopicUrl}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFileContent = fs.readFileSync(contentFilePath, 'utf8');
|
||||
const isFileEmpty = currentFileContent.replace(/^#.+/, ``).trim() === '';
|
||||
|
||||
if (!isFileEmpty) {
|
||||
console.log(`Ignoring ${topicId}. Not empty.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let newFileContent = `# ${topicTitle}`;
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
console.log(`Writing ${topicId}..`);
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
continue;
|
||||
}
|
||||
|
||||
const topicContent = await writeTopicContent(currTopicUrl);
|
||||
newFileContent += `\n\n${topicContent}`;
|
||||
|
||||
console.log(`Writing ${topicId}..`);
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
|
||||
// console.log(currentFileContent);
|
||||
// console.log(currTopicUrl);
|
||||
// console.log(topicTitle);
|
||||
// console.log(topicUrlToPathMapping[currTopicUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
.then(() => {
|
||||
console.log('Done');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
166
bin/roadmap-dirs.cjs
Normal file
@@ -0,0 +1,166 @@
|
||||
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/data/roadmaps');
|
||||
const roadmapId = process.argv[2];
|
||||
|
||||
const allowedRoadmapIds = fs.readdirSync(ROADMAP_CONTENT_DIR);
|
||||
if (!roadmapId) {
|
||||
console.error('roadmapId is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!allowedRoadmapIds.includes(roadmapId)) {
|
||||
console.error(`Invalid roadmap key ${roadmapId}`);
|
||||
console.error(`Allowed keys are ${allowedRoadmapIds.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Directory holding the roadmap content files
|
||||
const roadmapDirName = fs
|
||||
.readdirSync(ROADMAP_CONTENT_DIR)
|
||||
.find((dirName) => dirName.replace(/\d+-/, '') === roadmapId);
|
||||
|
||||
if (!roadmapDirName) {
|
||||
console.error('Roadmap directory not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const roadmapDirPath = path.join(ROADMAP_CONTENT_DIR, roadmapDirName);
|
||||
const roadmapContentDirPath = path.join(
|
||||
ROADMAP_CONTENT_DIR,
|
||||
roadmapDirName,
|
||||
'content'
|
||||
);
|
||||
|
||||
// If roadmap content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(roadmapContentDirPath)) {
|
||||
console.error(`Roadmap content already exists @ ${roadmapContentDirPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function prepareDirTree(control, dirTree, dirSortOrders) {
|
||||
// Directories are only created for groups
|
||||
if (control.typeID !== '__group__') {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. 104-testing-your-apps:other-options
|
||||
const controlName = control?.properties?.controlName || '';
|
||||
// e.g. 104
|
||||
const sortOrder = controlName.match(/^\d+/)?.[0];
|
||||
|
||||
// No directory for a group without control name
|
||||
if (!controlName || !sortOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// e.g. testing-your-apps:other-options
|
||||
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
|
||||
// e.g. ['testing-your-apps', 'other-options']
|
||||
const dirParts = controlNameWithoutSortOrder.split(':');
|
||||
|
||||
// Nest the dir path in the dirTree
|
||||
let currDirTree = dirTree;
|
||||
dirParts.forEach((dirPart) => {
|
||||
currDirTree[dirPart] = currDirTree[dirPart] || {};
|
||||
currDirTree = currDirTree[dirPart];
|
||||
});
|
||||
|
||||
dirSortOrders[controlNameWithoutSortOrder] = Number(sortOrder);
|
||||
|
||||
const childrenControls = control.children.controls.control;
|
||||
// No more children
|
||||
if (childrenControls.length) {
|
||||
childrenControls.forEach((childControl) => {
|
||||
prepareDirTree(childControl, dirTree, dirSortOrders);
|
||||
});
|
||||
}
|
||||
|
||||
return { dirTree, dirSortOrders };
|
||||
}
|
||||
|
||||
const roadmap = require(path.join(
|
||||
__dirname,
|
||||
`../public/jsons/roadmaps/${roadmapId}`
|
||||
));
|
||||
const controls = roadmap.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating and also calculate the sort orders
|
||||
const dirTree = {};
|
||||
const dirSortOrders = {};
|
||||
|
||||
controls.forEach((control) => {
|
||||
prepareDirTree(control, dirTree, dirSortOrders);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param parentDir Parent directory in which directory is to be created
|
||||
* @param dirTree Nested dir tree to be created
|
||||
* @param sortOrders Mapping from groupName to sort order
|
||||
* @param filePaths The mapping from groupName to file path
|
||||
*/
|
||||
function createDirTree(parentDir, dirTree, sortOrders, filePaths = {}) {
|
||||
const childrenDirNames = Object.keys(dirTree);
|
||||
const hasChildren = childrenDirNames.length !== 0;
|
||||
|
||||
// @todo write test for this, yolo for now
|
||||
const groupName = parentDir
|
||||
.replace(roadmapContentDirPath, '') // Remove base dir path
|
||||
.replace(/(^\/)|(\/$)/g, '') // Remove trailing slashes
|
||||
.replace(/(^\d+?-)/g, '') // Remove sorting information
|
||||
.replaceAll('/', ':') // Replace slashes with `:`
|
||||
.replace(/:\d+-/, ':');
|
||||
|
||||
const humanizedGroupName = groupName
|
||||
.split(':')
|
||||
.pop()
|
||||
?.replaceAll('-', ' ')
|
||||
.replace(/^\w/, ($0) => $0.toUpperCase());
|
||||
|
||||
const sortOrder = sortOrders[groupName] || '';
|
||||
|
||||
// Attach sorting information to dirname
|
||||
// e.g. /roadmaps/100-frontend/content/internet
|
||||
// ———> /roadmaps/100-frontend/content/103-internet
|
||||
if (sortOrder) {
|
||||
parentDir = parentDir.replace(/(.+?)([^\/]+)?$/, `$1${sortOrder}-$2`);
|
||||
}
|
||||
|
||||
// If no children, create a file for this under the parent directory
|
||||
if (!hasChildren) {
|
||||
let fileName = `${parentDir}.md`;
|
||||
fs.writeFileSync(fileName, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = fileName.replace(CONTENT_DIR, '');
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// There *are* children, so create the parent as a directory
|
||||
// and create `index.md` as the content file for this
|
||||
fs.mkdirSync(parentDir);
|
||||
|
||||
let readmeFilePath = path.join(parentDir, 'index.md');
|
||||
fs.writeFileSync(readmeFilePath, `# ${humanizedGroupName}`);
|
||||
|
||||
filePaths[groupName || 'home'] = readmeFilePath.replace(CONTENT_DIR, '');
|
||||
|
||||
// For each of the directory names, create a
|
||||
// directory inside the given directory
|
||||
childrenDirNames.forEach((dirName) => {
|
||||
createDirTree(
|
||||
path.join(parentDir, dirName),
|
||||
dirTree[dirName],
|
||||
dirSortOrders,
|
||||
filePaths
|
||||
);
|
||||
});
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// Create directories and get back the paths for created directories
|
||||
createDirTree(roadmapContentDirPath, dirTree, dirSortOrders);
|
||||
console.log('Created roadmap content directory structure');
|
||||
@@ -6,7 +6,7 @@ if (!roadmapId) {
|
||||
console.error('Error: roadmapId is required');
|
||||
}
|
||||
|
||||
const fullPath = path.join(__dirname, `../src/roadmaps/${roadmapId}`);
|
||||
const fullPath = path.join(__dirname, `../src/data/roadmaps/${roadmapId}`);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.error(`Error: path not found: ${fullPath}!`);
|
||||
process.exit(1);
|
||||
|
||||
171
bin/update-sponsors.cjs
Normal file
@@ -0,0 +1,171 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const apiKey = process.env.SPONSOR_SHEET_API_KEY;
|
||||
const sheetId = process.env.SPONSOR_SHEET_ID;
|
||||
|
||||
if (!apiKey || !sheetId) {
|
||||
console.error('Missing API key or sheet ID');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sheetRange = 'A3:I1001';
|
||||
const sheetUrl = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${sheetRange}?key=${apiKey}`;
|
||||
|
||||
function removeAllSponsors(baseContentDir) {
|
||||
console.log('------------------------');
|
||||
console.log('Removing sponsors from: ', baseContentDir);
|
||||
console.log('------------------------');
|
||||
const dataDirPath = path.join(__dirname, '../src/data');
|
||||
const contentDirPath = path.join(dataDirPath, baseContentDir);
|
||||
|
||||
const contentDir = fs.readdirSync(contentDirPath);
|
||||
contentDir.forEach((content) => {
|
||||
console.log('Removing sponsor from: ', content);
|
||||
|
||||
const pageFilePath = path.join(contentDirPath, content, `${content}.md`);
|
||||
const pageFileContent = fs.readFileSync(pageFilePath, 'utf8');
|
||||
|
||||
const frontMatterRegex = /---\n([\s\S]*?)\n---/;
|
||||
|
||||
const existingFrontmatter = pageFileContent.match(frontMatterRegex)[1];
|
||||
const contentWithoutFrontmatter = pageFileContent
|
||||
.replace(frontMatterRegex, ``)
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
delete frontmatterObj.sponsor;
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatterObj, {
|
||||
lineWidth: 10000,
|
||||
forceQuotes: true,
|
||||
quotingType: "'",
|
||||
});
|
||||
const newContent = `---\n${newFrontmatter}---\n${contentWithoutFrontmatter}`;
|
||||
|
||||
fs.writeFileSync(pageFilePath, newContent, 'utf8');
|
||||
});
|
||||
}
|
||||
|
||||
function addPageSponsor({
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
}) {
|
||||
const urlPart = pageUrl
|
||||
.replace('https://roadmap.sh/', '')
|
||||
.replace(/\?.+?$/, '');
|
||||
|
||||
const parentDir = urlPart.startsWith('best-practices/')
|
||||
? 'best-practices'
|
||||
: 'roadmaps';
|
||||
const pageId = urlPart.replace(`${parentDir}/`, '');
|
||||
|
||||
const pageFilePath = path.join(
|
||||
__dirname,
|
||||
`../src/data/${parentDir}`,
|
||||
`${pageId}/${pageId}.md`
|
||||
);
|
||||
|
||||
if (!fs.existsSync(pageFilePath)) {
|
||||
console.error(`Page file not found: ${pageFilePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Updating page: ${urlPart}`);
|
||||
const pageFileContent = fs.readFileSync(pageFilePath, 'utf8');
|
||||
|
||||
const frontMatterRegex = /---\n([\s\S]*?)\n---/;
|
||||
|
||||
const existingFrontmatter = pageFileContent.match(frontMatterRegex)[1];
|
||||
const contentWithoutFrontmatter = pageFileContent
|
||||
.replace(frontMatterRegex, ``)
|
||||
.trim();
|
||||
|
||||
let frontmatterObj = yaml.load(existingFrontmatter);
|
||||
delete frontmatterObj.sponsor;
|
||||
|
||||
const frontmatterValues = Object.entries(frontmatterObj);
|
||||
const roadmapLabel = frontmatterObj.briefTitle;
|
||||
|
||||
// Insert sponsor data at 10 index i.e. after
|
||||
// roadmap dimensions in the frontmatter
|
||||
frontmatterValues.splice(10, 0, [
|
||||
'sponsor',
|
||||
{
|
||||
url: redirectUrl,
|
||||
title: adTitle,
|
||||
imageUrl,
|
||||
description: adDescription,
|
||||
event: {
|
||||
category: 'SponsorClick',
|
||||
action: `${company} Redirect`,
|
||||
label: `${roadmapLabel} / ${company} Link`,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
frontmatterObj = Object.fromEntries(frontmatterValues);
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatterObj, {
|
||||
lineWidth: 10000,
|
||||
forceQuotes: true,
|
||||
quotingType: "'",
|
||||
});
|
||||
const newContent = `---\n${newFrontmatter}---\n\n${contentWithoutFrontmatter}`;
|
||||
|
||||
fs.writeFileSync(pageFilePath, newContent, 'utf8');
|
||||
}
|
||||
|
||||
// Remove sponsors from all roadmaps
|
||||
removeAllSponsors('roadmaps');
|
||||
removeAllSponsors('best-practices');
|
||||
|
||||
console.log('------------------------');
|
||||
console.log('Adding sponsors');
|
||||
console.log('------------------------');
|
||||
fetch(sheetUrl)
|
||||
.then((res) => res.json())
|
||||
.then((rawData) => {
|
||||
const rows = rawData.values;
|
||||
|
||||
rows.map((row) => {
|
||||
// prettier-ignore
|
||||
const [
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
] = row;
|
||||
|
||||
const isConfiguredActive = isActive?.toLowerCase() === 'yes';
|
||||
const currentDate = new Date();
|
||||
const isDateInRange =
|
||||
currentDate >= new Date(startDate) && currentDate <= new Date(endDate);
|
||||
|
||||
if (!isConfiguredActive || !isDateInRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
addPageSponsor({
|
||||
pageUrl,
|
||||
company,
|
||||
redirectUrl,
|
||||
imageUrl,
|
||||
adTitle,
|
||||
adDescription,
|
||||
startDate,
|
||||
endDate,
|
||||
isActive,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,21 +14,21 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
@@ -23,7 +23,7 @@ For the existing roadmaps, please follow the details listed for the nature of co
|
||||
|
||||
## Adding Content
|
||||
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
Find [the content directory inside the relevant roadmap](https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/roadmaps). Please keep the following guidelines in mind when submitting content:
|
||||
|
||||
- Content must be in English.
|
||||
- Put a brief description about the topic on top of the file and the a list of links below with each link having title of the URL.
|
||||
|
||||
28
package.json
@@ -8,33 +8,37 @@
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"format": "prettier --write .",
|
||||
"astro": "astro",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d dist -t",
|
||||
"compress:jsons": "node bin/compress-jsons.cjs",
|
||||
"upgrade": "ncu -u",
|
||||
"roadmap-links": "node bin/roadmap-links.cjs",
|
||||
"roadmap-dirs": "node bin/roadmap-dirs.cjs",
|
||||
"roadmap-content": "node bin/roadmap-content.cjs",
|
||||
"best-practice-content": "node bin/best-practice-content.cjs",
|
||||
"best-practice-dirs": "node bin/best-practice-dirs.cjs",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^1.0.0",
|
||||
"@astrojs/tailwind": "^2.1.3",
|
||||
"astro": "^1.9.2",
|
||||
"astro-compress": "^1.1.28",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"@astrojs/sitemap": "^1.2.1",
|
||||
"@astrojs/tailwind": "^3.1.1",
|
||||
"astro": "^2.1.7",
|
||||
"astro-compress": "^1.1.35",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"npm-check-updates": "^16.8.0",
|
||||
"rehype-external-links": "^2.0.1",
|
||||
"roadmap-renderer": "^1.0.4",
|
||||
"tailwindcss": "^3.2.4"
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@playwright/test": "^1.32.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"gh-pages": "^5.0.0",
|
||||
"json-to-pretty-yaml": "^1.2.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-astro": "^0.7.2"
|
||||
"openai": "^3.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-astro": "^0.8.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ const config: PlaywrightTestConfig = {
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: "http://localhost:3000",
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
};
|
||||
|
||||
5877
pnpm-lock.yaml
generated
BIN
public/best-practices/api-security.png
Normal file
|
After Width: | Height: | Size: 505 KiB |
BIN
public/best-practices/aws.png
Normal file
|
After Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
BIN
public/images/partners/kubecampus.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
1
public/jsons/best-practices/api-security.json
Normal file
1
public/jsons/best-practices/aws.json
Normal file
1
public/jsons/roadmaps/cyber-security.json
Normal file
1
public/jsons/roadmaps/frontend-beginner.json
Normal file
1
public/jsons/roadmaps/kubernetes.json
Normal file
1
public/jsons/roadmaps/mongodb.json
Normal file
1
public/jsons/roadmaps/ux-design.json
Normal file
BIN
public/pdfs/best-practices/api-security.pdf
Normal file
BIN
public/pdfs/best-practices/aws.pdf
Normal file
BIN
public/pdfs/roadmaps/cyber-security.pdf
Normal file
BIN
public/pdfs/roadmaps/kubernetes.pdf
Normal file
BIN
public/pdfs/roadmaps/mongodb.pdf
Normal file
BIN
public/pdfs/roadmaps/ux-design.pdf
Normal file
BIN
public/roadmaps/cyber-security.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/roadmaps/kubernetes.png
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
public/roadmaps/mongodb.png
Normal file
|
After Width: | Height: | Size: 528 KiB |
BIN
public/roadmaps/ux-design.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
11
readme.md
@@ -38,6 +38,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Software Architect Roadmap](https://roadmap.sh/software-architect)
|
||||
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
|
||||
- [JavaScript Roadmap](https://roadmap.sh/javascript)
|
||||
- [TypeScript Roadmap](https://roadmap.sh/typescript)
|
||||
- [React Roadmap](https://roadmap.sh/react)
|
||||
- [Vue Roadmap](https://roadmap.sh/vue)
|
||||
- [Angular Roadmap](https://roadmap.sh/angular)
|
||||
@@ -54,6 +55,16 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Blockchain Roadmap](https://roadmap.sh/blockchain)
|
||||
- [ASP.NET Core Roadmap](https://roadmap.sh/aspnet-core)
|
||||
- [System Design Roadmap](https://roadmap.sh/system-design)
|
||||
- [Kubernetes Roadmap](https://roadmap.sh/kubernetes)
|
||||
- [Cyber Security Roadmap](https://roadmap.sh/cyber-security)
|
||||
- [MongoDB Roadmap](https://roadmap.sh/mongodb)
|
||||
- [UX Design Roadmap](https://roadmap.sh/ux-design)
|
||||
|
||||
We have also added a new form of visual content covering best practices:
|
||||
|
||||
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
|
||||
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
|
||||
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
|
||||
|
||||

|
||||
|
||||
|
||||
23
sitemap.mjs
@@ -2,20 +2,21 @@ import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
async function getRoadmapIds() {
|
||||
return fs.readdir(path.join(process.cwd(), 'src/roadmaps'));
|
||||
return fs.readdir(path.join(process.cwd(), 'src/data/roadmaps'));
|
||||
}
|
||||
|
||||
async function getBestPracticesIds() {
|
||||
return fs.readdir(path.join(process.cwd(), 'src/best-practices'));
|
||||
return fs.readdir(path.join(process.cwd(), 'src/data/best-practices'));
|
||||
}
|
||||
|
||||
export function shouldIndexPage(page) {
|
||||
export function shouldIndexPage(pageUrl) {
|
||||
return ![
|
||||
'https://roadmap.sh/404',
|
||||
'https://roadmap.sh/terms',
|
||||
'https://roadmap.sh/privacy',
|
||||
'https://roadmap.sh/pdfs',
|
||||
].includes(page);
|
||||
'https://roadmap.sh/g',
|
||||
].includes(pageUrl);
|
||||
}
|
||||
|
||||
export async function serializeSitemap(item) {
|
||||
@@ -26,8 +27,13 @@ export async function serializeSitemap(item) {
|
||||
'https://roadmap.sh/best-practices',
|
||||
'https://roadmap.sh/guides',
|
||||
'https://roadmap.sh/videos',
|
||||
...(await getRoadmapIds()).flatMap((id) => [`https://roadmap.sh/${id}`, `https://roadmap.sh/${id}/topics`]),
|
||||
...(await getBestPracticesIds()).map((id) => `https://roadmap.sh/best-practices/${id}`),
|
||||
...(await getRoadmapIds()).flatMap((id) => [
|
||||
`https://roadmap.sh/${id}`,
|
||||
`https://roadmap.sh/${id}/topics`,
|
||||
]),
|
||||
...(await getBestPracticesIds()).map(
|
||||
(id) => `https://roadmap.sh/best-practices/${id}`
|
||||
),
|
||||
];
|
||||
|
||||
// Roadmaps and other high priority pages
|
||||
@@ -43,7 +49,10 @@ export async function serializeSitemap(item) {
|
||||
}
|
||||
|
||||
// Guide and video pages
|
||||
if (item.url.startsWith('https://roadmap.sh/guides') || item.url.startsWith('https://roadmap.sh/videos')) {
|
||||
if (
|
||||
item.url.startsWith('https://roadmap.sh/guides') ||
|
||||
item.url.startsWith('https://roadmap.sh/videos')
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
#
|
||||
@@ -1,90 +0,0 @@
|
||||
# Recommended Guides
|
||||
|
||||
> Optimize the critical rendering path:
|
||||
|
||||
* [Critical CSS? Not So Fast!](https://csswizardry.com/2022/09/critical-css-not-so-fast/)
|
||||
* [Priority Hints - What Your Browser Doesn’t Know (Yet)](https://www.etsy.com/codeascraft/priority-hints-what-your-browser-doesnt-know-yet)
|
||||
* [Optimizing resource loading with Priority Hints](https://web.dev/priority-hints/)
|
||||
* [Chrome Resource Priorities and Scheduling](https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit?usp=sharing)
|
||||
* [How To Optimize CSS for Peak Site Performance](https://kinsta.com/blog/optimize-css/)
|
||||
* [Eliminate render blocking CSS to improve start render time](https://www.jeffreyknox.dev/blog/eliminate-render-blocking-css-to-improve-start-render-time/)
|
||||
* [Small Bundles, Fast Pages: What To Do With Too Much JavaScript](https://calibreapp.com/blog/bundle-size-optimization)
|
||||
* [How to Eliminate Render-Blocking Resources: a Deep Dive](https://sia.codes/posts/render-blocking-resources/)
|
||||
* [The Critical Request: How to Prioritise Requests to Improve Speed](https://calibreapp.com/blog/critical-request)
|
||||
* [How to Improve CSS Performance](https://calibreapp.com/blog/css-performance)
|
||||
* [The Simplest Way to Load CSS Asynchronously](https://www.filamentgroup.com/lab/load-css-simpler/)
|
||||
* [CSS audit](https://css-tricks.com/a-quick-css-audit-and-general-notes-about-design-systems/)
|
||||
* [Measuring the Critical Rendering Path](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp)
|
||||
* [Inlining or Caching? Both Please!](https://www.filamentgroup.com/lab/inlining-cache.html)
|
||||
* [CSS and Network Performance](https://csswizardry.com/2018/11/css-and-network-performance/)
|
||||
* [Analyzing Critical Rendering Path Performance](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/analyzing-crp)
|
||||
* [Front-End Performance Checklist](https://github.com/thedaviddias/Front-End-Performance-Checklist)
|
||||
* [The PRPL Pattern](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)
|
||||
* [Now You See Me: How To Defer, Lazy-Load And Act With IntersectionObserver](https://www.smashingmagazine.com/2018/01/deferring-lazy-loading-intersection-observer-api/)
|
||||
* [Optimising the front end for the browser](https://hackernoon.com/optimising-the-front-end-for-the-browser-f2f51a29c572)
|
||||
* [Prefer DEFER Over ASYNC](https://calendar.perfplanet.com/2016/prefer-defer-over-async/)
|
||||
* [A comprehensive guide to font loading strategies](https://www.zachleat.com/web/comprehensive-webfonts/)
|
||||
* [Understanding the critical rendering path, rendering pages in 1 second](https://medium.com/@luisvieira_gmr/understanding-the-critical-rendering-path-rendering-pages-in-1-second-735c6e45b47a)
|
||||
* [More Weight Doesn’t Mean More Wait](https://www.filamentgroup.com/lab/weight-wait.html)
|
||||
|
||||
> JavaScript Rendering Performance
|
||||
|
||||
* [Five Data-Loading Patterns To Boost Web Performance](https://www.smashingmagazine.com/2022/09/data-loading-patterns-improve-frontend-performance/)
|
||||
* [Optimize long tasks](https://web.dev/optimize-long-tasks/)
|
||||
* [The impact of removing jQuery on our web performance](https://insidegovuk.blog.gov.uk/2022/08/15/the-impact-of-removing-jquery-on-our-web-performance/)
|
||||
* [Profiling & Optimizing the runtime performance with the DevTools Performance tab](iamtk.co/profiling-and-optimizing-the-runtime-performance-with-the-devtools-performance-tab)
|
||||
* [Don't fight the browser preload scanner](https://web.dev/preload-scanner/)
|
||||
* [The Web Performance impact of jQuery](https://twitter.com/TheRealNooshu/status/1509487050122276864)
|
||||
* [Have Single-Page Apps Ruined the Web? | Transitional Apps](https://www.youtube.com/watch?v=860d8usGC0o)
|
||||
* [Improve how you architect webapps](https://www.patterns.dev/)
|
||||
* [Nuxt SSR Optimizing Tips](https://vueschool.io/articles/vuejs-tutorials/nuxt-ssr-optimizing-tips/, Filip Rakowski
|
||||
* [GPU accelerated JavaScript](https://gpu.rocks/#/)
|
||||
* [Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker](https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp)
|
||||
* [Astro: Astro is a fresh but familiar approach to building websites. Astro combines decades of proven performance best practices with the DX improvements of the component-oriented era. Use your favorite JavaScript framework and automatically ship the bare-minimum amount of JavaScript—by default.](https://docs.astro.build/getting-started/)
|
||||
* [Minimising Layout and Layout thrashing for 60 FPS](https://www.charistheo.io/blog/2021/09/dom-reflow-and-layout-thrashing/)
|
||||
* [Does shadow DOM improve style performance?](https://nolanlawson.com/2021/08/15/does-shadow-dom-improve-style-performance/)
|
||||
* [Debugging memory leaks - HTTP 203](https://www.youtube.com/watch?v=YDU_3WdfkxA)
|
||||
* [Explore JavaScript Dependencies With Lighthouse Treemap](https://sia.codes/posts/lighthouse-treemap/)
|
||||
* [The real cost of Javascript dependencies (and the state of JS package quality)](https://medium.com/voodoo-engineering/the-real-cost-of-javascript-dependencies-and-the-state-of-js-package-quality-a8dacd74c0ec)
|
||||
* [The State Of Web Workers In 2021](https://www.smashingmagazine.com/2021/06/web-workers-2021/)
|
||||
* [Techniques for developing high-performance animations](https://web.dev/animations/)
|
||||
* [Building a Faster Web Experience with the postTask Scheduler](https://medium.com/airbnb-engineering/building-a-faster-web-experience-with-the-posttask-scheduler-276b83454e91), Callie (Airbnb Engineering & Data Science)
|
||||
* [Don’t attach tooltips to document.body – Learn how the browser works – Debug forced reflow](https://atfzl.com/don-t-attach-tooltips-to-document-body)
|
||||
* [How to Create and Fix Memory Leaks With Chrome DevTools](https://betterprogramming.pub/build-me-an-angular-app-with-memory-leaks-please-36302184e658)
|
||||
* [JavaScript performance beyond bundle size](https://nolanlawson.com/2021/02/23/javascript-performance-beyond-bundle-size/)
|
||||
* [The Import On Interaction Pattern](https://addyosmani.com/blog/import-on-interaction/)
|
||||
* [The “Live DOM” Is Not “Slow”, “Bad”, Or “Wrong”. Web Developers Are.](https://levelup.gitconnected.com/the-live-dom-is-not-slow-bad-or-wrong-web-developers-are-2bf86c3b9e2e)
|
||||
* [Prevent layout shifts with CSS grid stacks](https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/)
|
||||
* [content-visibility: the new CSS property that boosts your rendering performance](https://web.dev/content-visibility/)
|
||||
* [Preact vs React - Updating React at Etsy](https://github.com/mq2thez/blog/blob/main/upgrade-react-etsy/preact-vs-react.md)
|
||||
* [The Cost of Javascript Frameworks](https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/)
|
||||
* [Fixing memory leaks in web applications](https://nolanlawson.com/2020/02/19/fixing-memory-leaks-in-web-applications/)
|
||||
* [How to load polyfills only when needed](https://3perf.com/blog/polyfills/)
|
||||
* [Responsible JavaScript: Part III - Third parties](https://alistapart.com/article/responsible-javascript-part-3/)
|
||||
* [The cost of JavaScript in 2019](https://v8.dev/blog/cost-of-javascript-2019)
|
||||
* [When should you be using Web Workers?](https://dassur.ma/things/when-workers/)
|
||||
* [Responsible Javascript: Part II - Code Bundle](https://alistapart.com/article/responsible-javascript-part-2/)
|
||||
* [Faster script loading with BinaryAST?](https://blog.cloudflare.com/binary-ast/)
|
||||
* [Svelte 3: Rethinking reactivity](https://svelte.dev/blog/svelte-3-rethinking-reactivity)
|
||||
* [Responsible Javascript: Part I - Web platform over frameworks](https://alistapart.com/article/responsible-javascript-part-1/)
|
||||
* [JavaScript Loading Priorities in Chrome](https://addyosmani.com/blog/script-priorities/)
|
||||
* [Idle Until Urgent](https://philipwalton.com/articles/idle-until-urgent/)
|
||||
* [Browser painting and considerations for web performance](https://css-tricks.com/browser-painting-and-considerations-for-web-performance/)
|
||||
* [The Cost Of JavaScript In 2018](https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4) ([Video](https://www.youtube.com/watch?v=i5R7giitymk))
|
||||
* [Examining Web Worker Performance](https://www.loxodrome.io/post/web-worker-performance/)
|
||||
* [Front-End Performance Checklist](https://github.com/thedaviddias/Front-End-Performance-Checklist)
|
||||
* [jankfree](http://jankfree.org/)
|
||||
* [What forces layout/reflow?](https://gist.github.com/paulirish/5d52fb081b3570c81e3a)
|
||||
* [Using requestIdleCallback](https://developers.google.com/web/updates/2015/08/using-requestidlecallback)
|
||||
* [Optimize Javascript Execution](https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution)
|
||||
* [Why Web Developers Need to Care about Interactivity](https://philipwalton.com/articles/why-web-developers-need-to-care-about-interactivity/)
|
||||
* [Improving Performance with the Paint Timing API](https://www.sitepen.com/blog/2017/10/06/improving-performance-with-the-paint-timing-api)
|
||||
* [Deploying ES2015+ Code in Production Today](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)
|
||||
* [Performant Web Animations and Interactions: Achieving 60 FPS](https://blog.algolia.com/performant-web-animations/)
|
||||
* [JavaScript Start-up Performance](https://medium.com/reloading/javascript-start-up-performance-69200f43b201)
|
||||
* [Performant Parallaxing](https://developers.google.com/web/updates/2016/12/performant-parallaxing)
|
||||
* [The Anatomy of a Frame](https://aerotwist.com/blog/the-anatomy-of-a-frame/)
|
||||
* [The future of loading CSS](https://jakearchibald.com/2016/link-in-body/)
|
||||
* [4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them](https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/)
|
||||
* [The cost of frameworks](https://aerotwist.com/blog/the-cost-of-frameworks/)
|
||||
* [FLIP Your Animations](https://aerotwist.com/blog/flip-your-animations/)
|
||||
@@ -1,6 +0,0 @@
|
||||
# Use CDN
|
||||
|
||||
Use a CDN to serve your static assets. This will reduce the load on your server and improve the performance of your site.
|
||||
|
||||
- [10 Tips to Optimize CDN Performance - CDN Planet](https://www.cdnplanet.com/blog/10-tips-optimize-cdn-performance/)
|
||||
- [HTTP Caching | Web Fundamentals | Google Developers](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching)
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
jsonUrl: "/jsons/best-practices/frontend-performance.json"
|
||||
pdfUrl: "/pdfs/best-practices/frontend-performance.pdf"
|
||||
order: 1
|
||||
featuredTitle: "Frontend Performance"
|
||||
featuredDescription: "Frontend Performance Best Practices"
|
||||
isNew: true
|
||||
isUpcoming: false
|
||||
title: "Frontend Performance"
|
||||
description: "Detailed list of best practices to improve your frontend performance"
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 1270.89
|
||||
schema:
|
||||
headline: "Frontend Performance Best Practices"
|
||||
description: "Detailed list of best practices to improve the frontend performance of your website. Each best practice carries further details and how to implement that best practice."
|
||||
imageUrl: "https://roadmap.sh/best-practices/frontend-performance.png"
|
||||
datePublished: "2023-01-23"
|
||||
dateModified: "2023-01-23"
|
||||
seo:
|
||||
title: "Frontend Performance Best Practices"
|
||||
description: "Detailed list of best practices to improve the frontend performance of your website. Each best practice carries further details and how to implement that best practice."
|
||||
keywords:
|
||||
- "frontend performance"
|
||||
- "frontend performance best practices"
|
||||
- "frontend performance checklist"
|
||||
- "frontend checklist"
|
||||
- "make performant frontends"
|
||||
---
|
||||
@@ -2,6 +2,8 @@ export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// To selectively enable/disable debug logs
|
||||
__DEBUG__: boolean;
|
||||
gtag: any;
|
||||
fireEvent: (props: GAEventType) => void;
|
||||
}
|
||||
@@ -27,6 +29,10 @@ window.fireEvent = (props: GAEventType) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.__DEBUG__) {
|
||||
console.log('Analytics event fired', props);
|
||||
}
|
||||
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
|
||||
@@ -3,18 +3,16 @@ import Popup from './Popup/Popup.astro';
|
||||
import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
---
|
||||
|
||||
<Popup
|
||||
id='download-popup'
|
||||
title='Download'
|
||||
subtitle='Enter your email below to receive the download link.'
|
||||
>
|
||||
<Popup id='download-popup' title='Download' subtitle='Enter your email below to receive the download link.'>
|
||||
<form
|
||||
action='https://newsletter.roadmap.sh/subscribe'
|
||||
action='https://news.roadmap.sh/subscribe'
|
||||
method='POST'
|
||||
accept-charset='utf-8'
|
||||
target='_blank'
|
||||
captcha-form
|
||||
>
|
||||
<input type='hidden' name='gdpr' value='true' />
|
||||
|
||||
<input
|
||||
type='email'
|
||||
name='email'
|
||||
@@ -42,13 +40,11 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
</Popup>
|
||||
|
||||
<script>
|
||||
document
|
||||
.querySelector('[submit-download-form]')
|
||||
?.addEventListener('click', () => {
|
||||
window.fireEvent({
|
||||
category: 'Subscription',
|
||||
action: 'Submitted Popup Form',
|
||||
label: 'Download Roadmap Popup',
|
||||
});
|
||||
document.querySelector('[submit-download-form]')?.addEventListener('click', () => {
|
||||
window.fireEvent({
|
||||
category: 'Subscription',
|
||||
action: 'Submitted Popup Form',
|
||||
label: 'Download Roadmap Popup',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -19,13 +19,13 @@ if (faqs.length === 0) {
|
||||
}
|
||||
---
|
||||
|
||||
<div class='border-t bg-gray-100'>
|
||||
<div class='border-t bg-gray-100 mt-8'>
|
||||
<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'>
|
||||
<div class='flex flex-col gap-1 pb-14'>
|
||||
{
|
||||
faqs.map((faq, questionIndex) => (
|
||||
<Question isActive={questionIndex === 0} question={faq.question}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
import Loader from '../Loader.astro';
|
||||
import TopicOverlay from '../TopicOverlay/TopicOverlay.astro';
|
||||
import './FrameRenderer.css';
|
||||
|
||||
export interface Props {
|
||||
@@ -17,13 +16,15 @@ const { resourceId, resourceType, jsonUrl, dimensions = null } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
id='resource-svg'
|
||||
id='resource-svg-wrap'
|
||||
style={dimensions ? `--aspect-ratio:${dimensions.width}/${dimensions.height}` : null}
|
||||
data-resource-type={resourceType}
|
||||
data-resource-id={resourceId}
|
||||
data-json-url={jsonUrl}
|
||||
>
|
||||
<Loader />
|
||||
<div id='resource-loader'>
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./renderer.js'></script>
|
||||
|
||||
@@ -5,14 +5,21 @@ export class Renderer {
|
||||
this.resourceId = '';
|
||||
this.resourceType = '';
|
||||
this.jsonUrl = '';
|
||||
this.loaderHTML = null;
|
||||
|
||||
this.containerId = 'resource-svg';
|
||||
this.containerId = 'resource-svg-wrap';
|
||||
this.loaderId = 'resource-loader';
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
||||
this.jsonToSvg = this.jsonToSvg.bind(this);
|
||||
this.handleSvgClick = this.handleSvgClick.bind(this);
|
||||
this.prepareConfig = this.prepareConfig.bind(this);
|
||||
this.switchRoadmap = this.switchRoadmap.bind(this);
|
||||
}
|
||||
|
||||
get loaderEl() {
|
||||
return document.getElementById(this.loaderId);
|
||||
}
|
||||
|
||||
get containerEl() {
|
||||
@@ -24,6 +31,8 @@ export class Renderer {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clone it so we can use it later
|
||||
this.loaderHTML = this.loaderEl.innerHTML;
|
||||
const dataset = this.containerEl.dataset;
|
||||
|
||||
this.resourceType = dataset.resourceType;
|
||||
@@ -43,14 +52,30 @@ export class Renderer {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.containerEl.innerHTML = this.loaderHTML;
|
||||
|
||||
return fetch(jsonUrl)
|
||||
.then(function (res) {
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
.then((json) => {
|
||||
return wireframeJSONToSVG(json, {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
})
|
||||
.then((svg) => {
|
||||
this.containerEl.replaceChildren(svg);
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = `
|
||||
<strong>There was an error.</strong><br>
|
||||
|
||||
Try loading the page again. or submit an issue on GitHub with following:<br><br>
|
||||
|
||||
${error.message} <br /> ${error.stack}
|
||||
`;
|
||||
|
||||
this.containerEl.innerHTML = `<div class="error py-5 text-center text-red-600 mx-auto">${message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,11 +84,44 @@ export class Renderer {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsonToSvg(this.jsonUrl)
|
||||
.then((svg) => {
|
||||
document.getElementById(this.containerId).replaceChildren(svg);
|
||||
})
|
||||
.catch(console.error);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const roadmapType = urlParams.get('r');
|
||||
|
||||
if (roadmapType) {
|
||||
this.switchRoadmap(`/jsons/roadmaps/${roadmapType}.json`);
|
||||
} else {
|
||||
this.jsonToSvg(this.jsonUrl);
|
||||
}
|
||||
}
|
||||
|
||||
switchRoadmap(newJsonUrl) {
|
||||
const newJsonFileSlug = newJsonUrl.split('/').pop().replace('.json', '');
|
||||
|
||||
// Update the URL and attach the new roadmap type
|
||||
if (window?.history?.pushState) {
|
||||
const url = new URL(window.location);
|
||||
const type = this.resourceType[0]; // r for roadmap, b for best-practices
|
||||
|
||||
url.searchParams.delete(type);
|
||||
url.searchParams.set(type, newJsonFileSlug);
|
||||
|
||||
window.history.pushState(null, '', url.toString());
|
||||
}
|
||||
|
||||
const pageType = this.resourceType.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
|
||||
window.fireEvent({
|
||||
// RoadmapClick, BestPracticesClick, etc
|
||||
category: `${pageType.replace('-', '')}Click`,
|
||||
// roadmap/frontend/switch-version
|
||||
action: `${this.resourceId}/switch-version`,
|
||||
// roadmap/frontend/switch-version
|
||||
label: `${newJsonFileSlug}`,
|
||||
});
|
||||
|
||||
this.jsonToSvg(newJsonUrl).then(() => {
|
||||
this.containerEl.setAttribute('style', '');
|
||||
});
|
||||
}
|
||||
|
||||
handleSvgClick(e) {
|
||||
@@ -80,6 +138,14 @@ export class Renderer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^json:/.test(groupId)) {
|
||||
// e.g. /roadmaps/frontend-beginner.json
|
||||
const newJsonUrl = groupId.replace('json:', '');
|
||||
|
||||
this.switchRoadmap(newJsonUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^check:/.test(groupId)) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(`${this.resourceType}.topic.toggle`, {
|
||||
|
||||
@@ -32,14 +32,14 @@ const { author } = frontmatter;
|
||||
<span class='mx-1.5'>·</span>
|
||||
<a
|
||||
class='text-blue-400 hover:text-blue-500 hover:underline'
|
||||
href={`https://github.com/kamranahmedse/roadmap.sh/tree/master/src/guides/${guide.id}.md`}
|
||||
href={`https://github.com/kamranahmedse/roadmap.sh/tree/master/src/data/guides/${guide.id}.md`}
|
||||
target='_blank'>Improve this Guide</a
|
||||
>
|
||||
</p>
|
||||
<h1 class='text-2xl sm:text-5xl my-0 sm:my-3.5 font-bold'>
|
||||
{frontmatter.title}
|
||||
</h1>
|
||||
<p class='hidden sm:block text-gray-400 text-md'>
|
||||
<p class='hidden sm:block text-gray-400 text-xl'>
|
||||
{frontmatter.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class='prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-4 prose-h2:mb-2 prose-h3:mt-2 prose-img:mt-1'
|
||||
class='prose-xl prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-4 prose-h2:mb-2 prose-h3:mt-2 prose-img:mt-1'
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,9 @@ 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'>
|
||||
<h2 class='text-2xl sm:text-5xl font-bold'>Open Source</h2>
|
||||
<h2 class='text-2xl sm:text-5xl font-bold'>Community</h2>
|
||||
<p class='text-gray-600 text-sm sm:text-lg leading-relaxed my-2.5 sm:my-5'>
|
||||
The project is OpenSource, <a
|
||||
roadmap.sh is the <a
|
||||
href='https://github.com/search?o=desc&q=stars%3A%3E100000&s=stars&type=Repositories'
|
||||
target='_blank'
|
||||
class='font-medium text-gray-600 hover:text-black underline underline-offset-2'
|
||||
@@ -17,23 +17,25 @@ const starCount = await getFormattedStars('kamranahmedse/developer-roadmap');
|
||||
> and is visited by hundreds of thousands of developers every month.
|
||||
</p>
|
||||
|
||||
<div class='block mb-1.5 sm:mb-0'>
|
||||
<div class='flex justify-start flex-col sm:flex-row sm:justify-center gap-2 sm:gap-3 mb-1.5 sm:mb-0'>
|
||||
<a
|
||||
href='https://github.com/kamranahmedse/developer-roadmap'
|
||||
target='_blank'
|
||||
class='inline-flex items-center group rounded-md relative'
|
||||
class='inline-flex items-center border border-black py-1.5 px-3 rounded-lg text-sm hover:text-white hover:bg-black bg-white'
|
||||
>
|
||||
<span
|
||||
class='inline-flex items-center border border-black py-1.5 px-3 rounded-lg text-sm group-hover:text-white group-hover:bg-black relative bg-white'
|
||||
>
|
||||
<div class='mr-1 -ml-1'>
|
||||
<Icon icon='star' />
|
||||
</div>
|
||||
<Icon icon='star' class='mr-1 -ml-1 fill-current' />
|
||||
|
||||
<span class='lowercase'>{starCount}</span>
|
||||
<span class='ml-1.5 group-hover:hidden'>GitHub Stars</span>
|
||||
<span class='ml-2 hidden group-hover:block'>Star us on GitHub</span>
|
||||
</span>
|
||||
<span class='ml-2 hover:block'>GitHub Stars</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href='https://discord.gg/cJpEt5Qbwa'
|
||||
target='_blank'
|
||||
class='relative pointer inline-flex items-center border border-black py-1.5 px-3 rounded-lg text-sm hover:text-white hover:bg-black bg-white group'
|
||||
>
|
||||
<Icon icon='discord' class='h-[14px] mr-2 -ml-1 fill-current' />
|
||||
Join on Discord <span class="rounded-sm ml-0.5 px-1.5 py-0.5 text-xs uppercase">/ New</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
42
src/components/RelatedRoadmaps.astro
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
import { getRoadmapsByIds, RoadmapFrontmatter } from '../lib/roadmap';
|
||||
|
||||
export interface Props {
|
||||
roadmap: RoadmapFrontmatter;
|
||||
}
|
||||
|
||||
const { roadmap } = Astro.props;
|
||||
|
||||
const relatedRoadmaps = roadmap.relatedRoadmaps || [];
|
||||
if (!relatedRoadmaps.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relatedRoadmapDetails = await getRoadmapsByIds(relatedRoadmaps);
|
||||
---
|
||||
|
||||
<div class='border-t bg-gray-100'>
|
||||
<div class='container'>
|
||||
<div class='flex justify-between relative -top-5'>
|
||||
<h1 class='text-md font-medium py-1 px-3 border bg-white rounded-md'>Related Roadmaps</h1>
|
||||
<a href='/roadmaps' class='text-md font-medium py-1 px-3 border bg-white rounded-md hover:bg-gray-50'>
|
||||
<span class='hidden sm:inline'>All Roadmaps →</span>
|
||||
<span class='inline sm:hidden'>More →</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='flex flex-col gap-1 pb-8'>
|
||||
{
|
||||
relatedRoadmapDetails.map((relatedRoadmap) => (
|
||||
<a
|
||||
href={`/${relatedRoadmap.id}`}
|
||||
class='py-2 px-3.5 bg-white border rounded-md hover:bg-gray-50 flex flex-col sm:flex-row gap-0.5 sm:gap-0'
|
||||
>
|
||||
<span class='font-medium inline-block min-w-[150px]'>{relatedRoadmap.frontmatter.briefTitle}</span>
|
||||
<span class='text-gray-500'>{relatedRoadmap.frontmatter.briefDescription}</span>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,6 +17,6 @@ const { roadmap, roadmapId } = Astro.props;
|
||||
<span class='hidden sm:inline'>Click to visit the interactive version of</span>
|
||||
<span class='inline sm:hidden'>Visit complete</span>
|
||||
|
||||
<span class='sm:lowercase ml-0.5 font-medium underline underline-offset-1'>{roadmap.featuredTitle} roadmap</span>
|
||||
<span class='sm:lowercase ml-0.5 font-medium underline underline-offset-1'>{roadmap.briefTitle} roadmap</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -11,13 +11,14 @@ export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
note?: string;
|
||||
tnsBannerLink?: string;
|
||||
roadmapId: string;
|
||||
isUpcoming?: boolean;
|
||||
hasSearch?: boolean;
|
||||
hasTopics?: boolean;
|
||||
}
|
||||
|
||||
const { title, description, roadmapId, isUpcoming = false, hasSearch = false, note, hasTopics = false } = Astro.props;
|
||||
const { title, description, roadmapId, tnsBannerLink, isUpcoming = false, hasSearch = false, note, hasTopics = false } = Astro.props;
|
||||
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
---
|
||||
@@ -109,7 +110,7 @@ const isRoadmapReady = !isUpcoming;
|
||||
</div>
|
||||
|
||||
<!-- Desktop: Roadmap Resources - Alert -->
|
||||
{hasTopics && <RoadmapHint roadmapId={roadmapId} />}
|
||||
{hasTopics && <RoadmapHint roadmapId={roadmapId} tnsBannerLink={tnsBannerLink} />}
|
||||
|
||||
{hasSearch && <TopicSearch />}
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,12 @@ import Icon from './Icon.astro';
|
||||
|
||||
export interface Props {
|
||||
roadmapId: string;
|
||||
tnsBannerLink?: string;
|
||||
}
|
||||
|
||||
const { roadmapId } = Astro.props;
|
||||
const { roadmapId, tnsBannerLink = '' } = Astro.props;
|
||||
|
||||
const hasTNSBanner = ['frontend', 'backend', 'devops'].includes(roadmapId);
|
||||
const hasTNSBanner = !!tnsBannerLink;
|
||||
const roadmapTitle = roadmapId === 'devops' ? 'DevOps' : `${roadmapId.charAt(0).toUpperCase()}${roadmapId.slice(1)}`;
|
||||
---
|
||||
|
||||
@@ -26,7 +27,7 @@ const roadmapTitle = roadmapId === 'devops' ? 'DevOps' : `${roadmapId.charAt(0).
|
||||
<p class='text-sm'>
|
||||
Get the latest {roadmapTitle} news from our sister site{' '}
|
||||
<a
|
||||
href='https://thenewstack.io?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Alert'
|
||||
href={tnsBannerLink}
|
||||
target='_blank'
|
||||
class='font-semibold underline'
|
||||
ga-category='PartnerClick'
|
||||
|
||||
@@ -25,11 +25,11 @@ const {
|
||||
href={url}
|
||||
id='sponsor-ad'
|
||||
target='_blank'
|
||||
rel='noopener sponsored'
|
||||
rel='noopener sponsored nofollow'
|
||||
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'
|
||||
class='fixed bottom-[15px] right-[15px] outline-transparent z-50 bg-white max-w-[350px] shadow-lg outline-0 hidden'
|
||||
>
|
||||
<button
|
||||
class='absolute top-1.5 right-1.5 text-gray-300 hover:text-gray-800'
|
||||
|
||||
@@ -3,18 +3,16 @@ import Popup from './Popup/Popup.astro';
|
||||
import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
---
|
||||
|
||||
<Popup
|
||||
id='subscribe-popup'
|
||||
title='Subscribe'
|
||||
subtitle='Enter your email below to receive updates.'
|
||||
>
|
||||
<Popup id='subscribe-popup' title='Subscribe' subtitle='Enter your email below to receive updates.'>
|
||||
<form
|
||||
action='https://newsletter.roadmap.sh/subscribe'
|
||||
action='https://news.roadmap.sh/subscribe'
|
||||
method='POST'
|
||||
accept-charset='utf-8'
|
||||
target='_blank'
|
||||
captcha-form
|
||||
>
|
||||
<input type='hidden' name='gdpr' value='true' />
|
||||
|
||||
<input
|
||||
type='email'
|
||||
name='email'
|
||||
|
||||
@@ -22,6 +22,9 @@ const { contentContributionLink } = Astro.props;
|
||||
<div id='topic-actions' class='hidden mb-2'>
|
||||
<button
|
||||
id='mark-topic-done'
|
||||
ga-category='TopicClick'
|
||||
ga-action='topic/mark-completion'
|
||||
ga-label='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' />
|
||||
@@ -30,6 +33,9 @@ const { contentContributionLink } = Astro.props;
|
||||
|
||||
<button
|
||||
id='mark-topic-pending'
|
||||
ga-category='TopicClick'
|
||||
ga-action='topic/mark-completion'
|
||||
ga-label='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' />
|
||||
|
||||
@@ -15,9 +15,12 @@ export class Topic {
|
||||
this.activeTopicId = null;
|
||||
|
||||
this.handleRoadmapTopicClick = this.handleRoadmapTopicClick.bind(this);
|
||||
this.handleBestPracticeTopicClick = this.handleBestPracticeTopicClick.bind(this);
|
||||
this.handleBestPracticeTopicToggle = this.handleBestPracticeTopicToggle.bind(this);
|
||||
this.handleBestPracticeTopicPending = this.handleBestPracticeTopicPending.bind(this);
|
||||
this.handleBestPracticeTopicClick =
|
||||
this.handleBestPracticeTopicClick.bind(this);
|
||||
this.handleBestPracticeTopicToggle =
|
||||
this.handleBestPracticeTopicToggle.bind(this);
|
||||
this.handleBestPracticeTopicPending =
|
||||
this.handleBestPracticeTopicPending.bind(this);
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
this.resetDOM = this.resetDOM.bind(this);
|
||||
@@ -177,7 +180,13 @@ export class Topic {
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
this.resetDOM();
|
||||
this.renderTopicFromUrl(`/best-practices/${bestPracticeId}/${topicId.replaceAll(':', '/')}`);
|
||||
|
||||
const topicUrl = `/best-practices/${bestPracticeId}/${topicId.replaceAll(
|
||||
':',
|
||||
'/'
|
||||
)}`;
|
||||
|
||||
this.renderTopicFromUrl(topicUrl).then(() => null);
|
||||
}
|
||||
|
||||
handleRoadmapTopicClick(e) {
|
||||
@@ -192,31 +201,45 @@ export class Topic {
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
this.resetDOM();
|
||||
this.renderTopicFromUrl(`/${roadmapId}/${topicId.replaceAll(':', '/')}`);
|
||||
const topicUrl = `/${roadmapId}/${topicId.replaceAll(':', '/')}`;
|
||||
|
||||
window.fireEvent({
|
||||
category: `RoadmapClick`,
|
||||
action: `${roadmapId}/load-topic`,
|
||||
label: topicUrl,
|
||||
});
|
||||
|
||||
this.renderTopicFromUrl(topicUrl).then(() => null);
|
||||
}
|
||||
|
||||
querySvgElementsByTopicId(topicId) {
|
||||
const matchingElements = [];
|
||||
|
||||
// Elements having sort order in the beginning of the group id
|
||||
document.querySelectorAll(`[data-group-id$="-${topicId}"]`).forEach((element) => {
|
||||
const foundGroupId = element?.dataset?.groupId || '';
|
||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
|
||||
document
|
||||
.querySelectorAll(`[data-group-id$="-${topicId}"]`)
|
||||
.forEach((element) => {
|
||||
const foundGroupId = element?.dataset?.groupId || '';
|
||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
|
||||
|
||||
if (validGroupRegex.test(foundGroupId)) {
|
||||
matchingElements.push(element);
|
||||
}
|
||||
});
|
||||
if (validGroupRegex.test(foundGroupId)) {
|
||||
matchingElements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
// Elements with exact match of the topic id
|
||||
document.querySelectorAll(`[data-group-id="${topicId}"]`).forEach((element) => {
|
||||
matchingElements.push(element);
|
||||
});
|
||||
document
|
||||
.querySelectorAll(`[data-group-id="${topicId}"]`)
|
||||
.forEach((element) => {
|
||||
matchingElements.push(element);
|
||||
});
|
||||
|
||||
// Matching "check:XXXX" box of the topic
|
||||
document.querySelectorAll(`[data-group-id="check:${topicId}"]`).forEach((element) => {
|
||||
matchingElements.push(element);
|
||||
});
|
||||
document
|
||||
.querySelectorAll(`[data-group-id="check:${topicId}"]`)
|
||||
.forEach((element) => {
|
||||
matchingElements.push(element);
|
||||
});
|
||||
|
||||
return matchingElements;
|
||||
}
|
||||
@@ -247,29 +270,44 @@ export class Topic {
|
||||
return;
|
||||
}
|
||||
|
||||
const isClickedDone = e.target.id === this.markTopicDoneId || e.target.closest(`#${this.markTopicDoneId}`);
|
||||
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}`);
|
||||
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}`);
|
||||
const isClickedClose =
|
||||
e.target.id === this.closeTopicId ||
|
||||
e.target.closest(`#${this.closeTopicId}`);
|
||||
if (isClickedClose) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener('best-practice.topic.click', this.handleBestPracticeTopicClick);
|
||||
window.addEventListener('best-practice.topic.toggle', this.handleBestPracticeTopicToggle);
|
||||
window.addEventListener(
|
||||
'best-practice.topic.click',
|
||||
this.handleBestPracticeTopicClick
|
||||
);
|
||||
window.addEventListener(
|
||||
'best-practice.topic.toggle',
|
||||
this.handleBestPracticeTopicToggle
|
||||
);
|
||||
|
||||
window.addEventListener('roadmap.topic.click', this.handleRoadmapTopicClick);
|
||||
window.addEventListener(
|
||||
'roadmap.topic.click',
|
||||
this.handleRoadmapTopicClick
|
||||
);
|
||||
window.addEventListener('click', this.handleOverlayClick);
|
||||
window.addEventListener('contextmenu', this.rightClickListener);
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import CaptchaFields from './Captcha/CaptchaFields.astro';
|
||||
import Icon from './Icon.astro';
|
||||
---
|
||||
|
||||
<div class='my-0 px-5 rounded-lg text-left sm:text-center'>
|
||||
<div class='my-0 px-5 rounded-lg text-left sm:text-center sm:pb-10 pb-8'>
|
||||
<div class='sm:max-w-[400px] mx-auto'>
|
||||
<div class='hidden sm:block'><Icon icon='bell' /></div>
|
||||
<h2 class='text-3xl mb-1 font-medium hidden sm:block'>Upcoming</h2>
|
||||
<p class='text-gray-600 mb-0 sm:mb-5'>Please check back later or subscribe below.</p>
|
||||
|
||||
<form action='https://newsletter.roadmap.sh/subscribe' method='post' accept-charset='utf-8' captcha-form>
|
||||
<form action='https://news.roadmap.sh/subscribe' method='post' accept-charset='utf-8' captcha-form>
|
||||
<input
|
||||
type='email'
|
||||
required
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import Icon from './Icon.astro';
|
||||
---
|
||||
|
||||
<div class='sticky top-0 border-b border-b-yellow-300 z-20 flex h-[37px]' youtube-banner>
|
||||
<!-- sticky top-0 -->
|
||||
<div class='border-b border-b-yellow-300 z-20 flex h-[37px]' youtube-banner>
|
||||
<a
|
||||
href='https://youtube.com/theroadmap?sub_confirmation=1'
|
||||
target='_blank'
|
||||
|
||||
37
src/data/best-practices/api-security/api-security.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
jsonUrl: '/jsons/best-practices/api-security.json'
|
||||
pdfUrl: '/pdfs/best-practices/api-security.pdf'
|
||||
order: 2
|
||||
briefTitle: 'API Security'
|
||||
briefDescription: 'API Security Best Practices'
|
||||
isNew: true
|
||||
isUpcoming: false
|
||||
title: 'API Security Best Practices'
|
||||
description: 'Detailed list of best practices to make your APIs secure'
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 1543.39
|
||||
sponsor:
|
||||
url: 'https://liblab.com/blog/a-big-look-at-security-in-openapi?utm_source=roadmap_apisecruity&utm_medium=edge_stack&utm_campaign=april23'
|
||||
title: 'Secure APIs in OpenAPI'
|
||||
imageUrl: 'https://i.imgur.com/ZmuZUmS.png'
|
||||
description: 'Explore OpenAPI security options, industry best practices, and steps to secure your own API.'
|
||||
event:
|
||||
category: 'SponsorClick'
|
||||
action: 'Liblab Redirect'
|
||||
label: 'API Security / Liblab Link'
|
||||
schema:
|
||||
headline: 'API Security Best Practices'
|
||||
description: 'Detailed list of best practices to make your APIs secure. Each best practice carries further details and how to implement that best practice.'
|
||||
imageUrl: 'https://roadmap.sh/best-practices/api-security.png'
|
||||
datePublished: '2023-02-21'
|
||||
dateModified: '2023-02-21'
|
||||
seo:
|
||||
title: 'API Security Best Practices'
|
||||
description: 'Detailed list of best practices to make your APIs secure. Each best practice carries further details and how to implement that best practice.'
|
||||
keywords:
|
||||
- 'API Security'
|
||||
- 'API Security Best Practices'
|
||||
- 'API Security Checklist'
|
||||
---
|
||||
|
||||
15
src/data/best-practices/api-security/content/api-gateway.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# API Gateway
|
||||
|
||||
> Use an API Gateway for caching, Rate Limit policies, and other security features.
|
||||
|
||||
An API gateway can make your APIs more secure by providing a centralized point of control for managing and securing API traffic. Here are a few ways an API gateway can improve API security:
|
||||
|
||||
- Authentication and authorization: API gateways can handle user authentication and authorization, reducing the burden on individual APIs and improving consistency across the organization. This can include techniques such as JWT verification, OAuth, and other authentication mechanisms.
|
||||
|
||||
- Traffic filtering and rate limiting: An API gateway can enforce traffic filtering and rate limiting to protect APIs against DDoS attacks, brute force attacks, and other types of abuse.
|
||||
|
||||
- Encryption and decryption: An API gateway can handle encryption and decryption of sensitive data to protect against data breaches and theft.
|
||||
|
||||
- Logging and monitoring: An API gateway can provide centralized logging and monitoring of API traffic, helping to identify and respond to security threats and other issues.
|
||||
|
||||
- Integration with security tools: An API gateway can be integrated with security tools such as WAFs, SIEMs, and other security tools to provide additional layers of protection.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Authentication Mechanisms
|
||||
|
||||
> Use standard authentication mechanisms for generating tokens, storing credentials, and authenticating users.
|
||||
|
||||
Here are some examples of established authentication mechanisms that you can use instead of reinventing the wheel:
|
||||
|
||||
- OAuth: OAuth is a widely used open standard for authorization that enables users to grant third-party applications access to their resources without sharing their credentials. It is commonly used by web services and APIs to enable users to sign in with their social media accounts or other third-party accounts.
|
||||
|
||||
- OpenID Connect: OpenID Connect is an authentication protocol built on top of OAuth 2.0 that enables users to authenticate with multiple websites and applications using a single set of credentials. It is commonly used for single sign-on (SSO) across multiple websites and applications.
|
||||
|
||||
- SAML: Security Assertion Markup Language (SAML) is an XML-based standard for exchanging authentication and authorization data between parties. It is commonly used for SSO across multiple domains or organizations.
|
||||
|
||||
- Password hashing algorithms: Password hashing algorithms like bcrypt and scrypt are widely used to securely store and protect user passwords. These algorithms ensure that even if an attacker gains access to the password database, they will not be able to easily recover the passwords.
|
||||
|
||||
- Two-factor authentication (2FA): 2FA is an authentication mechanism that requires users to provide two forms of identification to access their accounts. This typically involves something the user knows (like a password) and something the user has (like a mobile device or security key). Many services and applications now offer 2FA as an additional security measure.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Authorization Header
|
||||
|
||||
> Use standard `Authorization` header for sending tokens instead of custom headers or query/body parameters
|
||||
|
||||
Sending tokens in the query or body parameters is generally not recommended because these parameters may be logged or cached by various systems, including web servers, proxies, and gateways. This can potentially lead to the exposure of sensitive data, including authentication tokens.
|
||||
|
||||
Additionally, sending tokens in query or body parameters can make them more vulnerable to cross-site request forgery (CSRF) attacks. In a CSRF attack, an attacker can trick a user into submitting a request that includes their authentication token, which the attacker can then use to impersonate the user and gain access to their account.
|
||||
|
||||
By contrast, using the `Authorization` header to send tokens helps to ensure that the tokens are not logged or cached by intermediary systems, and it can also help to protect against CSRF attacks by allowing the server to validate the token before processing the request.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Avoid HTTP Blocking
|
||||
|
||||
> Avoid HTTP blocking if you are using huge amount of data by moving the HTTP heavy operations to background jobs or asynchronous tasks.
|
||||
|
||||
HTTP blocking is a common issue in web applications. It occurs when the application is unable to process incoming HTTP requests due to a large number of requests or a large amount of data. This can lead to the application becoming unresponsive and the server crashing. This can be prevented by moving HTTP heavy operations to background jobs or asynchronous tasks. You can use a message queue to queue the requests and process them in the background. This will allow the application to continue processing other requests while the heavy operations are being processed in the background.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Avoid Logging Sensitive Data
|
||||
|
||||
> Ensure that you aren't logging any sensitive data.
|
||||
|
||||
Make sure that you are not logging any sensitive data such as passwords, credit card numbers, or personal information. This is because logging sensitive data can expose it to attackers, allowing them to gain unauthorized access to your system or data. Additionally, logging sensitive data can violate data privacy laws and regulations, exposing you to legal liability.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Avoid Personal ID in URLs
|
||||
|
||||
> Avoid user’s personal ID in the resource URLs e.g. users/242/orders
|
||||
|
||||
User's own resource ID should be avoided. Use `/me/orders` instead of `/user/654321/orders`. This will help avoid the risk of exposing the user’s personal ID that can be used for further attacks.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Avoid Returning Sensitive Data
|
||||
|
||||
> Only return the data that is needed for the client to function.
|
||||
|
||||
Returning only the data that is needed for the client to function is an important best practice for API security. This is because limiting the amount of data that is returned reduces the amount of sensitive information that is exposed. By only returning the necessary data, you can help prevent security vulnerabilities such as data leakage, injection attacks, and other types of attacks that rely on exposing too much information. Additionally, reducing the amount of data returned can improve the performance of your API by reducing the amount of data that needs to be processed and transmitted.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Use CDN for Uploads
|
||||
|
||||
> Use CDN for file uploads
|
||||
|
||||
Using a Content Delivery Network (CDN) for file uploads can make an API more secure by offloading the file upload traffic from the API server and reducing the risk of DDoS attacks.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Centralized Logins
|
||||
|
||||
> Use centralized logins for all services and components.
|
||||
|
||||
Using centralized logins for all services and components is important for several reasons:
|
||||
|
||||
- Centralized logins enable you to manage authentication and authorization in one place, reducing the risk of security gaps or inconsistencies across different services.
|
||||
|
||||
- Centralized logins provide a single point of entry, allowing you to control access and monitor activity more easily.
|
||||
|
||||
- Centralized logins make it easier to enforce security policies across different services and components, ensuring that only authorized users can access sensitive data or perform certain actions.
|
||||
|
||||
To use centralized logins, you need to set up a single sign-on (SSO) system that enables users to authenticate once and then access multiple services without having to provide credentials again. This can be done using protocols like OAuth or SAML, which enable secure authentication and authorization across different applications and services. Once set up, you can use centralized logging tools like ELK stack, Splunk, or Graylog to collect logs from different services and components and analyze them in one place. This enables you to quickly identify and respond to security threats or anomalies.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Dependencies
|
||||
|
||||
> Check your dependencies for known vulnerabilities and keep them up to date.
|
||||
|
||||
Vulnerabilities in third-party libraries and components can be exploited by attackers to gain access to your system or data. These vulnerabilities can be introduced through outdated or insecure dependencies that have not been updated with the latest security patches.
|
||||
|
||||
By regularly checking for vulnerabilities and keeping your dependencies up to date, you can ensure that your API is not susceptible to known security risks. This can be done by using automated tools or services that scan your codebase and provide reports on any vulnerabilities found in your dependencies. By addressing these vulnerabilities promptly, you can reduce the risk of your API being compromised by attackers.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Code Review Process
|
||||
|
||||
> Use a code review process and disregard self-approval.
|
||||
|
||||
Having a good code review process allows for additional sets of eyes to review the code and identify potential security issues or vulnerabilities. A code review process involves other team members reviewing the code to ensure it follows best practices and is secure. Disregarding self-approval means that the developer who wrote the code should not be the only one responsible for approving it for release. This helps to catch potential mistakes or oversights before the code is deployed, reducing the risk of security breaches or other issues.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Content Security Policy
|
||||
|
||||
> Send `Content-Security-Policy: default-src 'none'` header.
|
||||
|
||||
Sending the `Content-Security-Policy: default-src 'none'` header is a security best practice that helps prevent cross-site scripting (XSS) attacks. This header tells the browser to not allow any resources to be loaded from external sources, such as scripts, stylesheets, or images. It only allows resources that are explicitly whitelisted in the CSP header, such as scripts or stylesheets hosted on your own domain. This can help prevent malicious actors from injecting code into your web pages via XSS attacks, as the browser will not execute any scripts or load any resources that are not explicitly allowed by the CSP policy.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Turn Debug Mode Off
|
||||
|
||||
> Make sure to turn the debug mode off in production
|
||||
|
||||
Debug mode is a feature that is used to help developers debug their code. It is not meant to be used in production. It can expose sensitive information about the application and the server it is running on. Make sure to turn debug mode off in production.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Directory Listings
|
||||
|
||||
> Turn off directory listings
|
||||
|
||||
Directory listings are a feature of web servers that allow users to view the contents of a directory on a server. By default, web servers often have directory listings enabled, which means that anyone who has access to the server can see all the files and directories in a given folder.
|
||||
|
||||
Turning off directory listings is important for API security because it prevents attackers from gaining access to sensitive files and directories on the server. If directory listings are enabled and an attacker gains access to the server, they can easily view and download any files that are not properly protected. By disabling directory listings, you can ensure that only authorized users can access the files and directories on the server.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Disable Entity Expansion
|
||||
|
||||
> Disable entity expansion if using XML, YML or any other language
|
||||
|
||||
Disabling entity expansion is important when using XML, YAML, or any other language that allows entities because it helps prevent XXE (XML External Entity) or YAML tag injection attacks. In these attacks, attacker normally injects some sort of custom code in the input to perform attacks against the application.. By disabling entity expansion, the input cannot be manipulated in this way, reducing the risk of such attacks.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Disable Entinty Parsing in XML
|
||||
|
||||
> Disable entity parsing if you are parsing XML to avoid XXE attacks
|
||||
|
||||
If the XML parser is vulnerable to XXE attacks, the attacker can use this vulnerability to read files on the server, perform SSRF attacks, and more. This can lead to the disclosure of sensitive information, denial of service, and other attacks.
|
||||
|
||||
XXE (XML External Entity) attack is a type of attack that targets applications that parse XML input from untrusted sources. In this attack, an attacker injects a malicious XML payload. This payload can contain external entities that the attacker can use to retrieve sensitive data, execute remote code, or launch denial of service attacks. XXE attacks can be prevented by disabling external entity processing or by validating and sanitizing the XML input before parsing it.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Endpoint Authentication
|
||||
|
||||
> Check if all the protected endpoints are behind authentication
|
||||
> to avoid broken authentication process
|
||||
|
||||
By identifying and fixing broken authentication workflows, the API can prevent attacks such as brute force attacks, credential stuffing, session hijacking, and other authentication-related attacks. This can help ensure that the system is secure and that sensitive data is protected.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Force Content-Type
|
||||
|
||||
> Always force the `Content-Type` header to be set to relevant MIME type.
|
||||
|
||||
Forcing the content-type for API security is important because it ensures that the client and server are communicating in a mutually agreed-upon format for the data being transmitted. This can prevent attacks such as content spoofing or injection, where an attacker tries to trick the server into processing malicious content by pretending that it is of a different content type. By forcing the content-type to a specific format, the server can validate that the data it is receiving is legitimate and safe to process. Additionally, forcing the content-type can help prevent certain types of parsing errors that could be exploited by attackers.
|
||||
@@ -0,0 +1,5 @@
|
||||
# JWT Secret
|
||||
|
||||
> You should have a good JWT secret to protect against token tempering as well as avoiding brute force attacks.
|
||||
|
||||
A strong secret key should be randomly generated, long, and complex, and should be stored securely and rotated periodically.
|
||||
@@ -0,0 +1,9 @@
|
||||
# HSTS Header
|
||||
|
||||
> Use HSTS header with SSL to avoid SSL Strip attacks.
|
||||
|
||||
SSL strip is a type of attack where an attacker intercepts traffic between a client and a server that is meant to be secured by SSL/TLS encryption, and downgrades the connection to a plain text (non-encrypted) HTTP connection. This type of attack can go unnoticed by the user because the attacker is able to redirect the user to a look-alike website that also uses HTTP instead of HTTPS.
|
||||
|
||||
In an SSL strip attack, the attacker sets up a man-in-the-middle (MITM) position between the client and the server. When the client initiates a connection with the server, the attacker intercepts the SSL/TLS traffic and removes or replaces the HTTPS links with HTTP links. This can trick the user into thinking they are using a secure connection when in fact, they are not. The attacker can then monitor and manipulate the data transmitted between the client and server.
|
||||
|
||||
HSTS header is a security header that instructs browsers to only access the site over HTTPS. This header is used to prevent SSL Strip attacks. It is a good practice to use HSTS header with SSL.
|
||||
1
src/data/best-practices/api-security/content/index.md
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,5 @@
|
||||
# JWT Algorithm
|
||||
|
||||
> Do not extract the algorithm from the header, use backend.
|
||||
|
||||
Extracting the algorithm from the header of a JWT token can pose a security risk, as an attacker could modify the algorithm and potentially gain unauthorized access. It is therefore recommended to verify the algorithm on the backend rather than extracting it from the header. This can help ensure that the algorithm used to sign and verify the token is secure and has not been tampered with.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Jwt Payload
|
||||
|
||||
> Avoid storing sensitive data in JWT payload
|
||||
|
||||
Storing sensitive data in a JWT token payload can increase the risk of data breaches and other security incidents. If an attacker is able to obtain or tamper with the token, they could potentially access the sensitive data stored in the payload.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Max Retry/Jail in Login
|
||||
|
||||
> "Max Retry" and "jail" features are commonly used in login mechanisms to enhance security and prevent brute-force attacks.
|
||||
|
||||
**Max Retry:** The "Max Retry" feature limits the number of login attempts that a user can make within a specified time period. After a certain number of failed login attempts, the user is locked out of their account for a specified period of time, typically several minutes or hours. This helps to prevent brute-force attacks, where an attacker attempts to guess a user's password by making repeated login attempts. By limiting the number of attempts, the system can slow down or prevent such attacks.
|
||||
|
||||
**Jail:** The "jail" feature involves blocking IP addresses or user accounts that have exceeded the maximum number of failed login attempts within a certain time period. The blocked IP addresses or user accounts are prevented from attempting further logins for a specified period of time, typically several minutes or hours. This helps to prevent brute-force attacks, and also provides a mechanism to prevent malicious users from repeatedly attempting to access an account or system.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Monitor Everything
|
||||
|
||||
> Use agents to monitor all requests, responses and errors.
|
||||
|
||||
Using agents to monitor all requests, responses, and errors allows for real-time monitoring and detection of any abnormal activity or potential attacks. These agents can be configured to track metrics such as response times, error rates, and usage patterns, which can help identify any anomalies that could be indicative of an attack. By monitoring all requests and responses, the agents can provide visibility into the behavior of the API, which can help identify any potential security vulnerabilities or weaknesses. Additionally, agents can be used to log and analyze all data flowing through the API, which can be useful for debugging and auditing purposes.
|
||||
|
||||
To use agents for monitoring, a dedicated monitoring solution can be deployed alongside the API. This solution can be configured to capture data from all requests and responses, and analyze the data for any anomalies or issues. Agents can be implemented using various monitoring tools and technologies such as agents for application performance monitoring (APM), log monitoring, and network monitoring. The agents should be configured to provide real-time alerts to security teams if any suspicious activity is detected, allowing for immediate action to be taken.
|
||||
@@ -0,0 +1,7 @@
|
||||
# X-Content-Type-Options: nosniff
|
||||
|
||||
> Send `X-Content-Type-Options: nosniff` header.
|
||||
|
||||
You should send the `X-Content-Type-Options: nosniff` header to prevent [MIME type sniffing attacks](https://www.keycdn.com/support/what-is-mime-sniffing) on your web application. This header tells the browser not to override the response content type even if it's not the expected type. For example, if an attacker manages to upload an HTML file with a disguised extension like .jpg, the server may still send the correct content type header for the HTML file. However, some browsers may ignore this header and try to "sniff" the content type based on the actual contents of the file, leading to a potential cross-site scripting (XSS) attack.
|
||||
|
||||
By sending the `X-Content-Type-Options: nosniff` header, you tell the browser to always trust the provided content type and not try to sniff the content type. This helps to mitigate the risk of attackers exploiting content type mismatches to deliver malicious content to unsuspecting users.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Non-Executable Stacks
|
||||
|
||||
> Use non-executable stacks to prevent attackers from executing code on your server.
|
||||
|
||||
A stack usually refers to the call stack or execution stack. It is a data structure used by the computer program to manage and keep track of the sequence of function calls, local variables, and other related data during the execution of the program.
|
||||
|
||||
A non-executable stack is a security mechanism that prevents malicious code from being executed by preventing the stack memory from being executed as code. This helps to prevent attacks such as buffer overflow attacks, where an attacker tries to overwrite the return address on the stack to redirect the program to execute malicious code. By using non-executable stacks, the program can keep the stack separate from executable code and help prevent these types of attacks.
|
||||
@@ -0,0 +1,9 @@
|
||||
# redirect_uri
|
||||
|
||||
> Validate `redirect_uri’ on server-side to prevent open redirect attacks.
|
||||
|
||||
In OAuth, `redirect_uri` is a parameter that specifies the URI (Uniform Resource Identifier) that the authorization server should redirect the user to after authentication is complete. The `redirect_uri` is often used in the OAuth flow to return an authorization code or access token to the client application.
|
||||
|
||||
It is important to validate the `redirect_uri` on the server-side to prevent attacks such as open redirection attacks. In an open redirection attack, an attacker can modify the `redirect_uri` parameter to redirect the user to a malicious website. By validating the `redirect_uri` on the server-side, you can ensure that the redirect URI is a valid and authorized URI for the client application.
|
||||
|
||||
Validating the `redirect_uri` on the server-side can also prevent other types of attacks such as phishing attacks or cross-site request forgery (CSRF) attacks. By verifying that the `redirect_uri` matches a predefined list of authorized URIs, you can ensure that the user is redirected to a trusted site after authentication is complete.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Use State Param
|
||||
|
||||
> Use state parameter to avoid CSRF attacks
|
||||
|
||||
In OAuth, the `state` parameter is used as a security measure to prevent CSRF (Cross-Site Request Forgery) attacks. CSRF attacks occur when a malicious website or script sends a request to a legitimate website on behalf of a user who is currently authenticated.
|
||||
|
||||
To prevent CSRF attacks, the `state` parameter is used to store a unique value that is generated by the client application before initiating the authorization request. This value is included in the authorization request and then verified by the authorization server when the user is redirected back to the client application. If the `state` value in the authorization response matches the `state` value that was sent by the client application, the authorization is considered valid and the access token is returned to the client.
|
||||
|
||||
By using the `state` parameter, you can help to prevent malicious actors from intercepting or modifying the authorization request in transit, as the unique `state` value is only known to the client application and the authorization server. This can help to ensure the integrity and security of the OAuth flow and protect against CSRF attacks.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Validate Scope
|
||||
|
||||
> Have default scope, and validate scope for each application
|
||||
|
||||
In OAuth, scopes are used to specify the permissions and access levels that are granted to client applications when accessing protected resources on behalf of a user.
|
||||
|
||||
The best practice of having a default scope and validating the scope for each application is important because it helps to ensure that client applications only have access to the resources that they require, and that users are only granting the necessary permissions to each application.
|
||||
|
||||
The default scope is a set of permissions that are granted to all client applications by default, unless otherwise specified by the user. By having a default scope, you can ensure that all applications are subject to the same baseline security and access controls.
|
||||
|
||||
In addition to having a default scope, it is also recommended to validate the scope for each application. This means that when a user grants access to an application, the server should check to ensure that the requested scope is valid and appropriate for that application. This can help to prevent malicious applications from requesting excessive permissions or unauthorized access to user data.
|
||||
|
||||
By having a default scope and validating the scope for each application, you can help to ensure that the OAuth flow is secure and that client applications are only accessing the resources and permissions that they require.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Avoid Client-Side Encryption
|
||||
|
||||
> Use server-side encryption instead of client-side encryption
|
||||
|
||||
Client-side encryption is not recommended because client side codebase can be easily reverse engineered which can lead to the exposure of encryption algorithms.
|
||||
@@ -0,0 +1,5 @@
|
||||
# JWT Payload Size
|
||||
|
||||
> Avoid storing large payloads in JWT tokens
|
||||
|
||||
A smaller payload can reduce network overhead, improve processing speed, and decrease the risk of attacks aimed at overloading the system.
|
||||