diff --git a/.astro/settings.json b/.astro/settings.json
index 0124d4092..97ac9af57 100644
--- a/.astro/settings.json
+++ b/.astro/settings.json
@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
- "lastUpdateCheck": 1762257454897
+ "lastUpdateCheck": 1763378528944
}
}
\ No newline at end of file
diff --git a/.astro/types.d.ts b/.astro/types.d.ts
index f964fe0cf..03d7cc43f 100644
--- a/.astro/types.d.ts
+++ b/.astro/types.d.ts
@@ -1 +1,2 @@
///
+///
\ No newline at end of file
diff --git a/astro.config.mjs b/astro.config.mjs
index 6ecea3c3f..ecd628f5d 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -20,10 +20,29 @@ export default defineConfig({
status: 301,
destination: '/ai',
},
- },
- vite: {
- server: {
- allowedHosts: ['roadmap.sh', 'port3k.kamranahmed.info'],
+ '/best-practices': {
+ status: 301,
+ destination: '/roadmaps',
+ },
+ '/best-practices/aws': {
+ status: 301,
+ destination: '/aws-best-practices',
+ },
+ '/best-practices/backend-performance': {
+ status: 301,
+ destination: '/backend-performance-best-practices',
+ },
+ '/best-practices/frontend-performance': {
+ status: 301,
+ destination: '/frontend-performance-best-practices',
+ },
+ '/best-practices/api-security': {
+ status: 301,
+ destination: '/api-security-best-practices',
+ },
+ '/best-practices/code-review': {
+ status: 301,
+ destination: '/code-review-best-practices',
},
},
markdown: {
@@ -72,5 +91,8 @@ export default defineConfig({
ssr: {
noExternal: [/^@roadmapsh\/editor.*$/],
},
+ server: {
+ allowedHosts: ['roadmap.sh', 'port3k.kamranahmed.info'],
+ },
},
});
diff --git a/public/pdfs/roadmaps/api-security-best-practices.pdf b/public/pdfs/roadmaps/api-security-best-practices.pdf
new file mode 100644
index 000000000..1937f2ddc
Binary files /dev/null and b/public/pdfs/roadmaps/api-security-best-practices.pdf differ
diff --git a/public/pdfs/roadmaps/aws-best-practices.pdf b/public/pdfs/roadmaps/aws-best-practices.pdf
new file mode 100644
index 000000000..4527f12eb
Binary files /dev/null and b/public/pdfs/roadmaps/aws-best-practices.pdf differ
diff --git a/public/pdfs/roadmaps/backend-performance-best-practices.pdf b/public/pdfs/roadmaps/backend-performance-best-practices.pdf
new file mode 100644
index 000000000..8fd9007e8
Binary files /dev/null and b/public/pdfs/roadmaps/backend-performance-best-practices.pdf differ
diff --git a/public/pdfs/roadmaps/frontend-performance-best-practices.pdf b/public/pdfs/roadmaps/frontend-performance-best-practices.pdf
new file mode 100644
index 000000000..5a5bd332f
Binary files /dev/null and b/public/pdfs/roadmaps/frontend-performance-best-practices.pdf differ
diff --git a/public/roadmaps/api-security-best-practices.png b/public/roadmaps/api-security-best-practices.png
new file mode 100644
index 000000000..cee65e724
Binary files /dev/null and b/public/roadmaps/api-security-best-practices.png differ
diff --git a/public/roadmaps/aws-best-practices.png b/public/roadmaps/aws-best-practices.png
new file mode 100644
index 000000000..8baff4bfe
Binary files /dev/null and b/public/roadmaps/aws-best-practices.png differ
diff --git a/public/roadmaps/backend-performance-best-practices.png b/public/roadmaps/backend-performance-best-practices.png
new file mode 100644
index 000000000..0efdbcf23
Binary files /dev/null and b/public/roadmaps/backend-performance-best-practices.png differ
diff --git a/public/roadmaps/frontend-performance-best-practices.png b/public/roadmaps/frontend-performance-best-practices.png
new file mode 100644
index 000000000..03af889a4
Binary files /dev/null and b/public/roadmaps/frontend-performance-best-practices.png differ
diff --git a/src/components/CommandMenu/CommandMenu.tsx b/src/components/CommandMenu/CommandMenu.tsx
index 7ddc3ea1f..4e848d2ef 100644
--- a/src/components/CommandMenu/CommandMenu.tsx
+++ b/src/components/CommandMenu/CommandMenu.tsx
@@ -78,13 +78,6 @@ const defaultPages: PageType[] = [
icon: ,
isProtected: true,
},
- {
- id: 'best-practices',
- url: '/best-practices',
- title: 'Best Practices',
- group: 'Pages',
- icon: ,
- },
{
id: 'questions',
url: '/questions',
diff --git a/src/components/Dashboard/PersonalDashboard.tsx b/src/components/Dashboard/PersonalDashboard.tsx
index 2a19b9704..974468d61 100644
--- a/src/components/Dashboard/PersonalDashboard.tsx
+++ b/src/components/Dashboard/PersonalDashboard.tsx
@@ -453,12 +453,12 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
);
})}
diff --git a/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx b/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx
index 24a73e5e8..4213d89fd 100644
--- a/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx
+++ b/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx
@@ -176,6 +176,51 @@ export function EditorRoadmapRenderer(props: RoadmapRendererProps) {
return;
}
+ if (nodeType === 'checklist-item' && (target.tagName === 'text' || target.tagName === 'tspan')) {
+ e.preventDefault();
+
+ const textElement = target.tagName === 'tspan' ? (target.closest('text') as SVGTextElement) : target;
+ const clickedText = textElement?.textContent?.trim();
+ if (!clickedText) {
+ return;
+ }
+
+ const parentChecklistId = targetGroup?.dataset?.parentId;
+ if (!parentChecklistId) {
+ return;
+ }
+
+ const parentChecklistGroup = roadmapRef.current?.querySelector(
+ `g[data-node-id="${parentChecklistId}"][data-type="checklist"]`
+ );
+ if (!parentChecklistGroup) {
+ return;
+ }
+
+ const labelGroup = parentChecklistGroup.querySelector(
+ 'g[data-type="checklist-label"]'
+ );
+ if (!labelGroup) {
+ return;
+ }
+
+ const labelText = labelGroup.querySelector('text')?.textContent?.trim();
+ if (!labelText) {
+ return;
+ }
+
+ window.dispatchEvent(
+ new CustomEvent('roadmap.checklist.click', {
+ detail: {
+ roadmapId: resourceId,
+ labelText,
+ clickedText,
+ },
+ }),
+ );
+ return;
+ }
+
// we don't have the topic popup for checklist-item
if (nodeType === 'checklist-item') {
return;
diff --git a/src/components/FrameRenderer/RoadmapFloatingChat.tsx b/src/components/FrameRenderer/RoadmapFloatingChat.tsx
index f4f7d5f21..06dcd69d9 100644
--- a/src/components/FrameRenderer/RoadmapFloatingChat.tsx
+++ b/src/components/FrameRenderer/RoadmapFloatingChat.tsx
@@ -306,6 +306,53 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
};
}, [isOpen, isPersonalizeOpen]);
+ useEffect(() => {
+ const handleChecklistClick = (e: CustomEvent) => {
+ const { roadmapId: eventRoadmapId, labelText, clickedText } = e.detail;
+
+ if (eventRoadmapId !== roadmapId) {
+ return;
+ }
+
+ if (!isLoggedIn()) {
+ setIsOpen(false);
+ showLoginPopup();
+ return;
+ }
+
+ const roadmapTitle = roadmapDetail?.json?.title?.page || roadmapDetail?.json?.title?.card || 'this roadmap';
+ const message = `Explain the '${roadmapTitle}' best practice '${labelText} > ${clickedText}'`;
+
+ flushSync(() => {
+ setIsOpen(true);
+ });
+
+ setTimeout(() => {
+ sendMessage(
+ { text: message, metadata: { json: textToJSON(message) } },
+ {
+ body: {
+ roadmapId,
+ ...(activeChatHistoryId
+ ? { chatHistoryId: activeChatHistoryId }
+ : {}),
+ },
+ },
+ );
+
+ setTimeout(() => {
+ scrollToBottom('smooth');
+ inputRef.current?.focus();
+ }, 0);
+ }, 100);
+ };
+
+ window.addEventListener('roadmap.checklist.click', handleChecklistClick as EventListener);
+ return () => {
+ window.removeEventListener('roadmap.checklist.click', handleChecklistClick as EventListener);
+ };
+ }, [roadmapId, roadmapDetail, sendMessage, activeChatHistoryId, scrollToBottom]);
+
function textToJSON(text: string): JSONContent {
return {
type: 'doc',
diff --git a/src/components/Navigation/AccountDropdownList.tsx b/src/components/Navigation/AccountDropdownList.tsx
index f597c3a15..d29c184d7 100644
--- a/src/components/Navigation/AccountDropdownList.tsx
+++ b/src/components/Navigation/AccountDropdownList.tsx
@@ -81,9 +81,6 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
My Profile
-
- New
-
diff --git a/src/components/Roadmaps/RoadmapsPage.tsx b/src/components/Roadmaps/RoadmapsPage.tsx
index 2afd0dfaf..425079dd6 100644
--- a/src/components/Roadmaps/RoadmapsPage.tsx
+++ b/src/components/Roadmaps/RoadmapsPage.tsx
@@ -55,7 +55,7 @@ export type GroupType = {
roadmaps: {
title: string;
link: string;
- type: 'role' | 'skill';
+ type: 'role' | 'skill' | 'best-practice';
otherGroups?: AllowGroupNames[];
}[];
};
@@ -196,7 +196,12 @@ const groups: GroupType[] = [
title: 'JavaScript',
link: '/javascript',
type: 'skill',
- otherGroups: ['Web Development', 'DevOps', 'Mobile Development', 'Absolute Beginners'],
+ otherGroups: [
+ 'Web Development',
+ 'DevOps',
+ 'Mobile Development',
+ 'Absolute Beginners',
+ ],
},
{
title: 'Kotlin',
@@ -538,6 +543,41 @@ const groups: GroupType[] = [
},
],
},
+ {
+ group: 'Best Practices',
+ roadmaps: [
+ {
+ title: 'Backend Performance',
+ link: '/backend-performance-best-practices',
+ type: 'best-practice',
+ otherGroups: ['Web Development'],
+ },
+ {
+ title: 'Frontend Performance',
+ link: '/frontend-performance-best-practices',
+ type: 'best-practice',
+ otherGroups: ['Web Development'],
+ },
+ {
+ title: 'Code Review',
+ link: '/code-review-best-practices',
+ type: 'best-practice',
+ otherGroups: ['Web Development'],
+ },
+ {
+ title: 'AWS',
+ link: '/aws-review-best-practices',
+ type: 'best-practice',
+ otherGroups: ['Web Development', 'DevOps'],
+ },
+ {
+ title: 'API Security',
+ link: '/api-security-best-practices',
+ type: 'best-practice',
+ otherGroups: ['Web Development'],
+ },
+ ],
+ },
];
const roleRoadmaps = groups.flatMap((group) =>
@@ -546,6 +586,9 @@ const roleRoadmaps = groups.flatMap((group) =>
const skillRoadmaps = groups.flatMap((group) =>
group.roadmaps.filter((roadmap) => roadmap.type === 'skill'),
);
+const bestPracticeRoadmaps = groups.flatMap((group) =>
+ group.roadmaps.filter((roadmap) => roadmap.type === 'best-practice'),
+);
const allGroups = [
{
@@ -556,6 +599,10 @@ const allGroups = [
group: 'Skill Based Roadmaps',
roadmaps: skillRoadmaps,
},
+ {
+ group: 'Best Practices',
+ roadmaps: bestPracticeRoadmaps,
+ },
];
export function RoadmapsPage() {
diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro
index 88af1687a..e1d642a22 100644
--- a/src/pages/dashboard.astro
+++ b/src/pages/dashboard.astro
@@ -15,7 +15,7 @@ import type { BuiltInRoadmap } from '../components/Dashboard/PersonalDashboard';
const roadmaps = await listOfficialRoadmaps();
const roleRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'role');
const skillRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'skill');
-const bestPractices = await getAllBestPractices();
+const bestPractices = roadmaps.filter(roadmap => roadmap.type === 'best-practice');
const questionGroups = await getAllQuestionGroups();
const guides = await listOfficialGuides();
const videos = await getAllVideos();
@@ -48,15 +48,18 @@ const enrichedSkillRoadmaps: BuiltInRoadmap[] = skillRoadmaps.map((roadmap) => {
},
};
});
-
-const enrichedBestPractices = bestPractices.map((bestPractice) => {
- const { frontmatter } = bestPractice;
-
+const enrichedBestPracticeRoadmaps: BuiltInRoadmap[] = bestPractices.map((roadmap) => {
return {
- id: bestPractice.id,
- url: `/best-practices/${bestPractice.id}`,
- title: frontmatter.briefTitle,
- description: frontmatter.briefDescription,
+ id: roadmap.slug,
+ url: `/${roadmap.slug}`,
+ title: roadmap.title.card,
+ description: roadmap.description,
+ relatedRoadmapIds: roadmap.relatedRoadmaps,
+ renderer: 'editor',
+ isNew: isNewRoadmap(roadmap.createdAt),
+ metadata: {
+ tags: ['best-practice-roadmap'],
+ },
};
});
---
@@ -65,7 +68,7 @@ const enrichedBestPractices = bestPractices.map((bestPractice) => {
roadmap.type === 'role');
const skillRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'skill');
-const bestPractices = await getAllBestPractices();
+const bestPractices = roadmaps.filter(
+ (roadmap) => roadmap.type === 'best-practice',
+);
export const projectGroups = [
{
@@ -96,14 +97,17 @@ const videos = await getAllVideos();
({
- text: bestPractice.frontmatter.briefTitle,
- url: `/best-practices/${bestPractice.id}`,
- isNew: bestPractice.frontmatter.isNew,
- isUpcoming: bestPractice.frontmatter.isUpcoming,
- }))}
- />
+ featuredItems={bestPractices.map((bestPracticeItem) => {
+ const isNew = isNewRoadmap(bestPracticeItem.createdAt);
+ return {
+ text: bestPracticeItem.title.card,
+ url: `/${bestPracticeItem.slug}`,
+ isNew,
+ };
+ })}
+ showCreateRoadmap={false}
+ />