From 2d18cefd55a6adaa9555eb8489eed970b997f96d Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Mon, 1 Sep 2025 20:22:54 +0100 Subject: [PATCH] Revert "Revert "feat: official roadmap meta"" (#9096) * Revert "Revert "chore: update roadmap json endpoint"" This reverts commit 8dbe1468edb9e005d8bd93740122e7cfc4c1bbfe. * Revert "Revert "feat: roadmap main page"" This reverts commit bb13bf38a8018f47afd63938286953900ba3c798. * Revert "Revert "chore: replace roadmap listing"" This reverts commit 80dfd5b20633d152b9f129251733f0ac4c48deeb. * Revert "Revert "feat: roadmap courses"" This reverts commit a89c2d454f3d2acc7dcf0b7e4678f91bb7c48dd6. * Revert "Revert "fix: course length"" This reverts commit d1cf7cca99526432aa75396932ba256cc40c0709. * Revert "Revert "feat: roadmap with courses"" This reverts commit 9c32f9d46965d418f03cea760d9ef260ccb9b139. * Revert "Revert "chore: disable pre-render for roadmaps"" This reverts commit cef4c29f1034c6a73249e8aaa6e413a08bec1d96. --- .astro/types.d.ts | 1 + .../EditorRoadmap/EditorRoadmap.tsx | 7 +- src/components/FAQs/Answer.astro | 3 - src/components/FAQs/FAQs.astro | 42 ------ src/components/FAQs/FAQs.tsx | 61 ++++++++ src/components/FAQs/Question.astro | 42 ------ src/components/FAQs/Question.tsx | 29 ++++ src/components/GridItem.astro | 4 +- src/components/RelatedRoadmaps.astro | 75 ++-------- src/components/RoadmapHeader.astro | 10 -- src/components/RoadmapTitleQuestion.tsx | 24 ++-- src/data/roadmaps/ai-engineer/faqs.astro | 6 +- src/data/roadmaps/devops/faqs.astro | 2 +- src/data/roadmaps/frontend/faqs.astro | 52 +++---- src/data/roadmaps/full-stack/faqs.astro | 2 +- src/lib/guide-renderer.tsx | 4 +- src/lib/jsonld-schema.ts | 9 +- src/lib/markdown-renderer.tsx | 134 ++++++++++++++++++ src/lib/project.ts | 9 +- src/lib/roadmap.ts | 105 -------------- src/pages/404.astro | 27 ++-- src/pages/[roadmapId].json.ts | 57 ++------ src/pages/[roadmapId]/ai.astro | 43 ------ src/pages/[roadmapId]/courses.astro | 45 +++--- src/pages/[roadmapId]/index.astro | 129 ++++++++--------- src/pages/[roadmapId]/projects.astro | 41 ++---- src/pages/[roadmapId]/svg.astro | 55 +++---- src/pages/dashboard.astro | 77 +++++----- src/pages/index.astro | 50 ++++--- src/pages/pages.json.ts | 58 ++++---- src/pages/projects/[projectId]/index.astro | 18 +-- .../projects/[projectId]/solutions.astro | 19 +-- src/pages/projects/index.astro | 18 ++- src/pages/roadmaps.astro | 3 - src/queries/official-roadmap.ts | 125 +++++++++++++++- 35 files changed, 681 insertions(+), 705 deletions(-) delete mode 100644 src/components/FAQs/Answer.astro delete mode 100644 src/components/FAQs/FAQs.astro create mode 100644 src/components/FAQs/FAQs.tsx delete mode 100644 src/components/FAQs/Question.astro create mode 100644 src/components/FAQs/Question.tsx create mode 100644 src/lib/markdown-renderer.tsx delete mode 100644 src/pages/[roadmapId]/ai.astro 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/src/components/EditorRoadmap/EditorRoadmap.tsx b/src/components/EditorRoadmap/EditorRoadmap.tsx index 997a78362..dc5defaf0 100644 --- a/src/components/EditorRoadmap/EditorRoadmap.tsx +++ b/src/components/EditorRoadmap/EditorRoadmap.tsx @@ -23,7 +23,12 @@ type EditorRoadmapProps = { }; export function EditorRoadmap(props: EditorRoadmapProps) { - const { resourceId, resourceType = 'roadmap', dimensions, hasChat = true } = props; + const { + resourceId, + resourceType = 'roadmap', + dimensions, + hasChat = true, + } = props; const [hasSwitchedRoadmap, setHasSwitchedRoadmap] = useState(false); const [isLoading, setIsLoading] = useState(true); diff --git a/src/components/FAQs/Answer.astro b/src/components/FAQs/Answer.astro deleted file mode 100644 index 88a9d731a..000000000 --- a/src/components/FAQs/Answer.astro +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/src/components/FAQs/FAQs.astro b/src/components/FAQs/FAQs.astro deleted file mode 100644 index f6df90815..000000000 --- a/src/components/FAQs/FAQs.astro +++ /dev/null @@ -1,42 +0,0 @@ ---- -import { markdownToHtml } from '../../lib/markdown'; -import Answer from './Answer.astro'; -import Question from './Question.astro'; - -export type FAQType = { - question: string; - answer: string[]; -}; - -export interface Props { - faqs: FAQType[]; -} - -const { faqs } = Astro.props; - -if (faqs.length === 0) { - return ''; -} ---- - -
-
-
-

Frequently Asked Questions

-
- -
- { - faqs.map((faq, questionIndex) => ( - - - {faq.answer.map((answer) => ( -

- ))} - - - )) - } -

-
-
diff --git a/src/components/FAQs/FAQs.tsx b/src/components/FAQs/FAQs.tsx new file mode 100644 index 000000000..b80aae3c9 --- /dev/null +++ b/src/components/FAQs/FAQs.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import type { OfficialRoadmapQuestion } from '../../queries/official-roadmap'; +import { Question } from './Question'; +import { guideRenderer } from '../../lib/guide-renderer'; + +type FAQsProps = { + faqs: OfficialRoadmapQuestion[]; +}; + +export function FAQs(props: FAQsProps) { + const { faqs } = props; + if (faqs.length === 0) { + return null; + } + + const [activeQuestionIndex, setActiveQuestionIndex] = useState(0); + + return ( +
+
+
+

+ Frequently Asked Questions +

+
+ +
+ {faqs.map((faq, questionIndex) => { + const isTextDescription = + typeof faq?.description === 'string' && + faq?.description?.length > 0; + + return ( + setActiveQuestionIndex(questionIndex)} + > +
+ {!isTextDescription + ? guideRenderer.render(faq.description) + : null} +
+
+ ); + })} +
+
+
+ ); +} diff --git a/src/components/FAQs/Question.astro b/src/components/FAQs/Question.astro deleted file mode 100644 index e933aac0c..000000000 --- a/src/components/FAQs/Question.astro +++ /dev/null @@ -1,42 +0,0 @@ ---- -import Icon from '../AstroIcon.astro'; - -export interface Props { - question: string; - isActive?: boolean; -} - -const { question, isActive = false } = Astro.props; ---- - -
- -
- -
-
- - diff --git a/src/components/FAQs/Question.tsx b/src/components/FAQs/Question.tsx new file mode 100644 index 000000000..cb65fb74a --- /dev/null +++ b/src/components/FAQs/Question.tsx @@ -0,0 +1,29 @@ +import { cn } from '../../lib/classname'; +import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon'; + +type QuestionProps = { + question: string; + isActive?: boolean; + children: React.ReactNode; + onClick?: () => void; +}; + +export function Question(props: QuestionProps) { + const { question, isActive = false, children, onClick } = props; + + return ( +
+ + + {isActive &&
{children}
} +
+ ); +} diff --git a/src/components/GridItem.astro b/src/components/GridItem.astro index 2b80746b4..7a327cf1c 100644 --- a/src/components/GridItem.astro +++ b/src/components/GridItem.astro @@ -1,6 +1,4 @@ --- -import type { RoadmapFileType } from '../lib/roadmap'; - export interface Props { url: string; title: string; @@ -27,7 +25,7 @@ const { url, title, description, isNew } = Astro.props; { isNew && ( - + diff --git a/src/components/RelatedRoadmaps.astro b/src/components/RelatedRoadmaps.astro index 919e64f3b..1eecaf64a 100644 --- a/src/components/RelatedRoadmaps.astro +++ b/src/components/RelatedRoadmaps.astro @@ -1,69 +1,22 @@ --- -import { getQuestionGroupsByIds } from '../lib/question-group'; -import { getRoadmapsByIds, type RoadmapFrontmatter } from '../lib/roadmap'; -import { Map, Clipboard } from 'lucide-react'; +import { Map } from 'lucide-react'; +import { listOfficialRoadmaps } from '../queries/official-roadmap'; export interface Props { - roadmap: RoadmapFrontmatter; + relatedRoadmaps: string[]; } -const { roadmap } = Astro.props; +const { relatedRoadmaps } = Astro.props; -const relatedRoadmaps = roadmap.relatedRoadmaps || []; -const relatedRoadmapDetails = await getRoadmapsByIds(relatedRoadmaps); - -const relatedQuestions = roadmap.relatedQuestions || []; -const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions); +const allRoadmaps = await listOfficialRoadmaps(); +const relatedRoadmapsDetails = allRoadmaps.filter((roadmap) => + relatedRoadmaps.includes(roadmap.slug), +); --- -{ - relatedQuestionDetails.length > 0 && ( -
-
-
- - - Test your Knowledge - - - - More → - -
- -
- {relatedQuestionDetails.map((relatedQuestionGroup) => ( - - - {relatedQuestionGroup.title} - - - {relatedQuestionGroup.description} - - - ))} -
-
-
- ) -} - { relatedRoadmaps.length && ( -
+
@@ -80,17 +33,15 @@ const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions);
- {relatedRoadmapDetails.map((relatedRoadmap) => ( + {relatedRoadmapsDetails.map((relatedRoadmap) => ( - {relatedRoadmap.frontmatter.briefTitle} - - - {relatedRoadmap.frontmatter.briefDescription} + {relatedRoadmap.title.card} + {relatedRoadmap.description} ))}
diff --git a/src/components/RoadmapHeader.astro b/src/components/RoadmapHeader.astro index 31d42d9d8..17fff115a 100644 --- a/src/components/RoadmapHeader.astro +++ b/src/components/RoadmapHeader.astro @@ -5,9 +5,7 @@ import { Bot, FolderKanbanIcon, MapIcon, - MessageCircle, } from 'lucide-react'; -import { type RoadmapFrontmatter } from '../lib/roadmap'; import LoginPopup from './AuthenticationFlow/LoginPopup.astro'; import { DownloadRoadmapButton } from './DownloadRoadmapButton'; import { MarkFavorite } from './FeaturedItems/MarkFavorite'; @@ -20,20 +18,16 @@ import { PersonalizedRoadmap } from './PersonalizedRoadmap/PersonalizedRoadmap'; export interface Props { title: string; description: string; - note?: string; partner?: { description: string; link: string; linkText: string; }; roadmapId: string; - isUpcoming?: boolean; hasSearch?: boolean; projectCount?: number; coursesCount?: number; hasAIChat?: boolean; - question?: RoadmapFrontmatter['question']; - hasTopics?: boolean; isForkable?: boolean; activeTab?: 'roadmap' | 'projects' | 'courses'; } @@ -43,12 +37,8 @@ const { description, roadmapId, partner, - isUpcoming = false, - note, - hasTopics = false, hasAIChat = false, projectCount = 0, - question, activeTab = 'roadmap', coursesCount = 0, } = Astro.props; diff --git a/src/components/RoadmapTitleQuestion.tsx b/src/components/RoadmapTitleQuestion.tsx index bb6daf50f..139a1b58e 100644 --- a/src/components/RoadmapTitleQuestion.tsx +++ b/src/components/RoadmapTitleQuestion.tsx @@ -10,10 +10,12 @@ import { useOutsideClick } from '../hooks/use-outside-click'; import { markdownToHtml } from '../lib/markdown'; import { cn } from '../lib/classname'; import { useScrollPosition } from '../hooks/use-scroll-position'; +import type { JSONContent } from '@tiptap/core'; +import { guideRenderer } from '../lib/guide-renderer'; type RoadmapTitleQuestionProps = { question: string; - answer: string; + answer: JSONContent; roadmapId?: string; }; @@ -38,24 +40,24 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) { 'rounded-0 -mx-4 sm:mx-0': isAnswerVisible, // @FIXME: // The line below is to keep the question hidden on mobile devices except for - // the frontend roadmap. This is because we did not use to have the question + // the frontend roadmap. This is because we did not use to have the question // on mobile devices before and we don't want to cause any SEO issues. It will // be enabled on other roadmaps in the future. }, )} > {isAnswerVisible && ( -
+
)}

{ e.preventDefault(); setIsAnswerVisible(!isAnswerVisible); }} > - + {question} @@ -65,7 +67,7 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {

{ setIsAnswerVisible(false); @@ -95,9 +97,11 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) { )}
+ className="bg-gray-100 p-3 text-base [&>h2]:mt-5 [&>h2]:mb-2 [&>h2]:text-[17px] [&>h2]:font-medium [&>p]:mb-3 [&>p]:leading-relaxed [&>p]:font-normal [&>p]:text-gray-800 [&>p:last-child]:mb-0 [&>p>a]:font-semibold [&>p>a]:underline [&>p>a]:underline-offset-2 [&>ul>li]:mb-2 [&>ul>li]:font-normal" + // dangerouslySetInnerHTML={{ __html: markdownToHtml(answer, false) }} + > + {guideRenderer.render(answer)} +
); diff --git a/src/data/roadmaps/ai-engineer/faqs.astro b/src/data/roadmaps/ai-engineer/faqs.astro index f7ee7ddf6..2a7948d11 100644 --- a/src/data/roadmaps/ai-engineer/faqs.astro +++ b/src/data/roadmaps/ai-engineer/faqs.astro @@ -1,5 +1,5 @@ --- -import type { FAQType } from '../../../components/FAQs/FAQs.astro'; +import type { FAQType } from '../../../components/FAQs/FAQs'; export const faqs: FAQType[] = [ { @@ -11,13 +11,13 @@ export const faqs: FAQType[] = [ { question: 'What is reinforcement learning?', answer: [ - '[Reinforcement learning](https://towardsdatascience.com/reinforcement-learning-101-e24b50e1d292) (RL) is a type of machine learning where an agent learns to make decisions by interacting with an environment. Unlike traditional supervised learning, RL does not rely on labeled data. Instead, the agent learns by taking actions and receiving feedback in the form of rewards or penalties. Over time, it aims to maximize cumulative rewards by refining its strategy based on past experiences. RL is often used in areas like robotics, game AI, and autonomous systems, where the goal is to develop intelligent behaviors through trial and error.', + '[Reinforcement learning](https://towardsdatascience.com/reinforcement-learning-101-e24b50e1d292) (RL) is a type of machine learning where an agent learns to make decisions by interacting with an environment. Unlike traditional supervised learning, RL does not rely on labeled data. Instead, the agent learns by taking actions and receiving feedback in the form of rewards or penalties. Over time, it aims to maximize cumulative rewards by refining its strategy based on past experiences. RL is often used in areas like robotics, game AI, and autonomous systems, where the goal is to develop intelligent behaviors through trial and error.', ], }, { question: 'Do AI Engineers need a degree?', answer: [ - 'While a degree in computer science, data science, or a related field can provide a solid foundation for becoming an AI engineer, it is not strictly necessary. Many successful AI engineers are self-taught or have gained expertise through online courses, certifications, and hands-on projects.' + 'While a degree in computer science, data science, or a related field can provide a solid foundation for becoming an AI engineer, it is not strictly necessary. Many successful AI engineers are self-taught or have gained expertise through online courses, certifications, and hands-on projects.', ], }, ]; diff --git a/src/data/roadmaps/devops/faqs.astro b/src/data/roadmaps/devops/faqs.astro index 7b8959cbb..5743d375e 100644 --- a/src/data/roadmaps/devops/faqs.astro +++ b/src/data/roadmaps/devops/faqs.astro @@ -1,5 +1,5 @@ --- -import type { FAQType } from '../../../components/FAQs/FAQs.astro'; +import type { FAQType } from '../../../components/FAQs/FAQs'; export const faqs: FAQType[] = [ { diff --git a/src/data/roadmaps/frontend/faqs.astro b/src/data/roadmaps/frontend/faqs.astro index 25759f207..72a824a39 100644 --- a/src/data/roadmaps/frontend/faqs.astro +++ b/src/data/roadmaps/frontend/faqs.astro @@ -1,50 +1,50 @@ --- -import type { FAQType } from '../../../components/FAQs/FAQs.astro'; +import type { FAQType } from '../../../components/FAQs/FAQs'; export const faqs: FAQType[] = [ { question: 'Is Frontend Development really coding?', answer: [ - "Do frontend developers really code? The answer is yes, absolutely.", - "The fact that frontend developers are full-time developers who produce an output that is visually appealing (thanks to the designs provided by others) sometimes confuses others, making them believe that frontend developers aren\’t really coding. However, that couldn\’t be further from the truth.", - "As a frontend developer, you\’ll be coding all the time.", - "While in some companies, the frontend developer is also a skilled designer or UX engineer, those are not the typical profiles. As a frontend dev, your learning focus should be coding-related (i.e coding best practices, software design patterns, frontend architecture, etc).", + 'Do frontend developers really code? The answer is yes, absolutely.', + 'The fact that frontend developers are full-time developers who produce an output that is visually appealing (thanks to the designs provided by others) sometimes confuses others, making them believe that frontend developers aren\’t really coding. However, that couldn\’t be further from the truth.', + 'As a frontend developer, you\’ll be coding all the time.', + 'While in some companies, the frontend developer is also a skilled designer or UX engineer, those are not the typical profiles. As a frontend dev, your learning focus should be coding-related (i.e coding best practices, software design patterns, frontend architecture, etc).', ], }, { question: 'Is Frontend Developer a good career?', answer: [ - "As the web space and technology in general continue to evolve, the role of frontend developers becomes more relevant. In the end, providing a web version of an application has quite a lot of benefits, making it the obvious version (unless there is a specific requirement forcing for native development) for most systems, meaning that frontend developers have the potential to be involved in all types of projects and companies.", - "This renders the frontend developer career one of the most versatile and in-demand paths in the web tech industry.", + 'As the web space and technology in general continue to evolve, the role of frontend developers becomes more relevant. In the end, providing a web version of an application has quite a lot of benefits, making it the obvious version (unless there is a specific requirement forcing for native development) for most systems, meaning that frontend developers have the potential to be involved in all types of projects and companies.', + 'This renders the frontend developer career one of the most versatile and in-demand paths in the web tech industry.', ], }, { question: 'How to prepare for a frontend developer interview?', answer: [ - "If you\’re looking to prepare for a frontend developer interview, make sure you\’ve tackled the fundamentals of the technologies you\’ll work with.", - "And while that is one of the most impactful things you can do for your frontend career, consider focusing on the following points as well:", - "1. **Master the Fundamentals**: Ensure a solid understanding of HTML, CSS, and JavaScript, as they are crucial for the role of frontend developer.", - "2. **Practice Coding**: Improve your skills through mini-projects or coding challenges on platforms like LeetCode, especially those focused on front-end development. Using these skills to create a [web developer portfolio](https://roadmap.sh/frontend/web-developer-portfolio) can help you in many ways.", - "3. **Learn Modern Frameworks**: Familiarize yourself with popular frameworks like React, Angular, or Vue.js.", - "4. **Know Your Tools**: Be comfortable with version control systems, testing, and build tools, which are vital for all types of development, including frontend.", - "5. **Understand UI/UX Principles**: Learn about accessibility, responsive design, and intuitive interfaces to stand out.", - "6. **Research the Company**: Show interest by learning about the company\’s business and products.", - "7. **Enhance Communication Skills**: Good communication skills are essential for success in any role, so make sure to work on them.", + 'If you\’re looking to prepare for a frontend developer interview, make sure you\’ve tackled the fundamentals of the technologies you\’ll work with.', + 'And while that is one of the most impactful things you can do for your frontend career, consider focusing on the following points as well:', + '1. **Master the Fundamentals**: Ensure a solid understanding of HTML, CSS, and JavaScript, as they are crucial for the role of frontend developer.', + '2. **Practice Coding**: Improve your skills through mini-projects or coding challenges on platforms like LeetCode, especially those focused on front-end development. Using these skills to create a [web developer portfolio](https://roadmap.sh/frontend/web-developer-portfolio) can help you in many ways.', + '3. **Learn Modern Frameworks**: Familiarize yourself with popular frameworks like React, Angular, or Vue.js.', + '4. **Know Your Tools**: Be comfortable with version control systems, testing, and build tools, which are vital for all types of development, including frontend.', + '5. **Understand UI/UX Principles**: Learn about accessibility, responsive design, and intuitive interfaces to stand out.', + '6. **Research the Company**: Show interest by learning about the company\’s business and products.', + '7. **Enhance Communication Skills**: Good communication skills are essential for success in any role, so make sure to work on them.', ], }, { question: 'How is Frontend Development different from Backend Development?', answer: [ - "The main difference between frontend development and backend development is that frontend development focuses on the UI, while [backend development](https://roadmap.sh/backend) focuses more on the server-side logic.", - "You see, frontend development works with the user interface and user experience, dealing with the design, layout, and interactivity of a website or application using HTML, CSS, and JavaScript (or TypeScript).", - "On the other hand, backend development handles the server-side logic, databases, and application functionality, ensuring data is processed and served correctly. The tech stack for backend development is much bigger with more options, such as Python, Java, or Node.js.", - "Both options are equally interesting and challenging, so it\’s not really a question of backend vs frontend, but instead of understanding where you feel more comfortable and what type of systems you enjoy creating.", + 'The main difference between frontend development and backend development is that frontend development focuses on the UI, while [backend development](https://roadmap.sh/backend) focuses more on the server-side logic.', + 'You see, frontend development works with the user interface and user experience, dealing with the design, layout, and interactivity of a website or application using HTML, CSS, and JavaScript (or TypeScript).', + 'On the other hand, backend development handles the server-side logic, databases, and application functionality, ensuring data is processed and served correctly. The tech stack for backend development is much bigger with more options, such as Python, Java, or Node.js.', + 'Both options are equally interesting and challenging, so it\’s not really a question of backend vs frontend, but instead of understanding where you feel more comfortable and what type of systems you enjoy creating.', ], }, { question: 'What are the job titles of a Frontend Developer?', answer: [ - "Front-end developers are also known as front-end engineers, front-end web developers, JavaScript Developers, HTML/CSS Developer, front-end web designers, and front-end web architects.", + 'Front-end developers are also known as front-end engineers, front-end web developers, JavaScript Developers, HTML/CSS Developer, front-end web designers, and front-end web architects.', "Each of these roles mostly encompasses the same front-end development skills but requires different levels of expertise in different [front-end development skills](https://roadmap.sh/frontend/developer-skills). It's better to look at the job description to get an idea about the job requirements.", ], }, @@ -57,16 +57,16 @@ export const faqs: FAQType[] = [ { question: 'How long does it take to become a Frontend Developer?', answer: [ - "The amount of time it takes to become a frontend developer can vary depending on several factors, such as your learning pace, previous experience, and the amount of time you are able to dedicate to learning.", + 'The amount of time it takes to become a frontend developer can vary depending on several factors, such as your learning pace, previous experience, and the amount of time you are able to dedicate to learning.', "However, to give you a rough idea, if you are a complete beginner, it could take you anywhere from 3 to 6 months to get a job as an entry-level frontend developer. If you are already familiar with some of the frontend technologies, it could take you anywhere from 1 to 3 months. What's important is to practice as much as you can while you are learning i.e., by building as many projects as you can. You should also participate in online communities and ask for feedback from more experienced developers to accelerate your learning process.", ], }, { question: 'What are the Frontend Developer salaries?', answer: [ - "Frontend developer salaries can vary depending on factors such as location, experience, and company size. According to data from Glassdoor, the average base salary for a frontend developer in the United States is around $80,000 per year. However, this number can vary greatly depending on location, with the highest-paying cities such as San Francisco, Seattle, and New York having an average salary of $110,000 to $130,000.", + 'Frontend developer salaries can vary depending on factors such as location, experience, and company size. According to data from Glassdoor, the average base salary for a frontend developer in the United States is around $80,000 per year. However, this number can vary greatly depending on location, with the highest-paying cities such as San Francisco, Seattle, and New York having an average salary of $110,000 to $130,000.', "It's important to keep in mind that these are just averages, and salaries can vary greatly depending on factors such as experience level, specific skills, and the company you work for. With more experience and specific skills, you can expect to earn more.", - "It is worth looking at a range of resources, including salary surveys and job boards, to get a general understanding of the current market in your location and experience level. Also, try reaching out to other professionals in the field and understanding their experience and salary ranges.", + 'It is worth looking at a range of resources, including salary surveys and job boards, to get a general understanding of the current market in your location and experience level. Also, try reaching out to other professionals in the field and understanding their experience and salary ranges.', ], }, { @@ -79,8 +79,8 @@ export const faqs: FAQType[] = [ { question: 'What are Frontend Frameworks?', answer: [ - "[Frontend frameworks](https://roadmap.sh/frontend/frameworks) are collections of tools and libraries that help developers build web applications more efficiently. They provide a structure for the code, making it easier to build and maintain complex applications.", - "Some popular frontend frameworks include React, Angular, and Vue.js. These frameworks provide a set of tools and libraries that help developers build user interfaces, manage state, and interact with APIs.", + '[Frontend frameworks](https://roadmap.sh/frontend/frameworks) are collections of tools and libraries that help developers build web applications more efficiently. They provide a structure for the code, making it easier to build and maintain complex applications.', + 'Some popular frontend frameworks include React, Angular, and Vue.js. These frameworks provide a set of tools and libraries that help developers build user interfaces, manage state, and interact with APIs.', ], }, ]; diff --git a/src/data/roadmaps/full-stack/faqs.astro b/src/data/roadmaps/full-stack/faqs.astro index d1ae72244..26c36472e 100644 --- a/src/data/roadmaps/full-stack/faqs.astro +++ b/src/data/roadmaps/full-stack/faqs.astro @@ -1,5 +1,5 @@ --- -import type { FAQType } from '../../../components/FAQs/FAQs.astro'; +import type { FAQType } from '../../../components/FAQs/FAQs'; export const faqs: FAQType[] = [ { diff --git a/src/lib/guide-renderer.tsx b/src/lib/guide-renderer.tsx index 78f05e29c..e13c7dd8d 100644 --- a/src/lib/guide-renderer.tsx +++ b/src/lib/guide-renderer.tsx @@ -30,10 +30,12 @@ export interface MarkType { attrs?: Record | undefined; } +export type GuideRendererOptions = {}; + export class GuideRenderer { private marksOrder = ['underline', 'bold', 'italic', 'textStyle', 'link']; - render(content: JSONContent): JSX.Element[] { + render(content: JSONContent) { const nodes = content.content || []; const jsxNodes = nodes .map((node, index) => { diff --git a/src/lib/jsonld-schema.ts b/src/lib/jsonld-schema.ts index a3f8ca1fa..8f9823b6e 100644 --- a/src/lib/jsonld-schema.ts +++ b/src/lib/jsonld-schema.ts @@ -1,4 +1,5 @@ -import type { FAQType } from '../components/FAQs/FAQs.astro'; +import type { OfficialRoadmapQuestion } from '../queries/official-roadmap'; +import { renderMarkdownFromJson } from './markdown-renderer'; type ArticleSchemaProps = { url: string; @@ -41,16 +42,16 @@ export function generateArticleSchema(article: ArticleSchemaProps) { }; } -export function generateFAQSchema(faqs: FAQType[]) { +export function generateFAQSchema(faqs: OfficialRoadmapQuestion[]) { return { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqs.map((faq) => ({ '@type': 'Question', - name: faq.question, + name: faq.title, acceptedAnswer: { '@type': 'Answer', - text: faq.answer.join(' '), + text: renderMarkdownFromJson(faq.description, { join: ' ' }), }, })), }; diff --git a/src/lib/markdown-renderer.tsx b/src/lib/markdown-renderer.tsx new file mode 100644 index 000000000..24924cfb7 --- /dev/null +++ b/src/lib/markdown-renderer.tsx @@ -0,0 +1,134 @@ +import type { JSONContent } from '@tiptap/core'; + +export type MarkdownRendererOptions = { + join?: string; +}; + +export class MarkdownRenderer { + private marksOrder = ['underline', 'bold', 'italic', 'textStyle', 'link']; + + render(content: JSONContent, options: MarkdownRendererOptions = {}): string { + const nodes = content.content || []; + return nodes + .map((node) => this.renderNode(node)) + .join(options?.join || '\n\n'); + } + + private renderNode(node: JSONContent): string { + const type = node.type || ''; + + if (type in this) { + // @ts-expect-error dynamic lookup + return this[type]?.(node); + } + + console.warn(`Node type "${type}" is not supported.`); + return ''; + } + + private getText(node: JSONContent): string { + if (node.type === 'text') return node.text || ''; + if (node.content) + return node.content.map((child) => this.getText(child)).join(''); + return ''; + } + + private content(node: JSONContent): string { + return (node.content || []).map((child) => this.renderNode(child)).join(''); + } + + private renderMark(node: JSONContent): string { + let text = node.text || ''; + let marks = node.marks || []; + marks.sort( + (a, b) => + this.marksOrder.indexOf(a.type) - this.marksOrder.indexOf(b.type), + ); + + return marks.reduce((acc, mark) => { + if (mark.type === 'bold') return `**${acc}**`; + if (mark.type === 'italic') return `*${acc}*`; + if (mark.type === 'underline') return `_${acc}_`; // fallback since markdown has no underline + if (mark.type === 'code') return `\`${acc}\``; + if (mark.type === 'link') return `[${acc}](${mark.attrs?.href})`; + return acc; + }, text); + } + + // ---- Nodes ---- + private paragraph(node: JSONContent): string { + return this.content(node); + } + + private text(node: JSONContent): string { + return node.marks ? this.renderMark(node) : node.text || ''; + } + + private heading(node: JSONContent): string { + const level = node.attrs?.level || 1; + const prefix = '#'.repeat(level); + return `${prefix} ${this.content(node)}`; + } + + private bulletList(node: JSONContent): string { + return (node.content || []) + .map((child) => `- ${this.renderNode(child)}`) + .join('\n'); + } + + private orderedList(node: JSONContent): string { + return (node.content || []) + .map((child, i) => `${i + 1}. ${this.renderNode(child)}`) + .join('\n'); + } + + private listItem(node: JSONContent): string { + return this.content(node); + } + + private blockquote(node: JSONContent): string { + return this.content(node) + .split('\n') + .map((line) => `> ${line}`) + .join('\n'); + } + + private codeBlock(node: JSONContent): string { + const code = this.getText(node); + const language = node.attrs?.language || ''; + return `\`\`\`${language}\n${code}\n\`\`\``; + } + + private horizontalRule(): string { + return `---`; + } + + private image(node: JSONContent): string { + const { src, alt } = node.attrs || {}; + return `![${alt || ''}](${src})`; + } + + private table(node: JSONContent): string { + const rows = (node.content || []).filter((n) => n.type === 'tableRow'); + return rows.map((row) => this.renderNode(row)).join('\n'); + } + + private tableRow(node: JSONContent): string { + return `| ${this.content(node)} |`; + } + + private tableHeader(node: JSONContent): string { + return this.content(node); + } + + private tableCell(node: JSONContent): string { + return this.content(node); + } +} + +export function renderMarkdownFromJson( + json: JSONContent, + options: MarkdownRendererOptions = {}, +) { + return new MarkdownRenderer().render(json, options); +} diff --git a/src/lib/project.ts b/src/lib/project.ts index 41cef4841..1a16aea66 100644 --- a/src/lib/project.ts +++ b/src/lib/project.ts @@ -1,5 +1,8 @@ +import { + officialRoadmapDetails, + type OfficialRoadmapDocument, +} from '../queries/official-roadmap'; import type { MarkdownFileType } from './file'; -import { getRoadmapById, type RoadmapFileType } from './roadmap'; export const projectDifficulties = [ 'beginner', @@ -28,7 +31,7 @@ export interface ProjectFrontmatter { export type ProjectFileType = MarkdownFileType & { id: string; - roadmaps: RoadmapFileType[]; + roadmaps: OfficialRoadmapDocument[]; }; /** @@ -85,7 +88,7 @@ export async function getProjectById( const project = await import(`../data/projects/${groupId}.md`); const roadmapIds = project.frontmatter.roadmapIds || []; const roadmaps = await Promise.all( - roadmapIds.map((roadmapId: string) => getRoadmapById(roadmapId)), + roadmapIds.map((roadmapId: string) => officialRoadmapDetails(roadmapId)), ); return { diff --git a/src/lib/roadmap.ts b/src/lib/roadmap.ts index c83d9c41c..0beb7a18f 100644 --- a/src/lib/roadmap.ts +++ b/src/lib/roadmap.ts @@ -1,19 +1,7 @@ import type { PageType } from '../components/CommandMenu/CommandMenu'; -import type { MarkdownFileType } from './file'; import { httpGet } from './http'; import type { ResourceType } from './resource-progress'; -export function resourceTitleFromId(id: string): string { - if (id === 'devops') { - return 'DevOps'; - } - - return id - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); -} - export type AllowedRoadmapRenderer = 'balsamiq' | 'editor'; export interface RoadmapFrontmatter { @@ -76,99 +64,6 @@ export interface RoadmapFrontmatter { renderer?: AllowedRoadmapRenderer; } -export type RoadmapFileType = MarkdownFileType & { - id: string; -}; - -function roadmapPathToId(filePath: string): string { - const fileName = filePath.split('/').pop() || ''; - - return fileName.replace('.md', ''); -} - -/** - * Gets the IDs of all the roadmaps available on the website - * - * @returns string[] Array of roadmap IDs - */ -export async function getRoadmapIds() { - const roadmapFiles = import.meta.glob( - '/src/data/roadmaps/*/*.md', - { - eager: true, - }, - ); - - return Object.keys(roadmapFiles).map(roadmapPathToId); -} - -/** - * Gets the roadmap files which have the given tag assigned - * - * @param tag Tag assigned to roadmap - * @returns Promisified RoadmapFileType[] - */ -export async function getRoadmapsByTag( - tag: string, -): Promise { - const roadmapFilesMap = import.meta.glob( - '/src/data/roadmaps/*/*.md', - { - eager: true, - }, - ); - - const roadmapFiles: RoadmapFileType[] = Object.values(roadmapFilesMap); - const filteredRoadmaps = roadmapFiles - .filter((roadmapFile) => roadmapFile.frontmatter.tags?.includes(tag)) - .map((roadmapFile) => ({ - ...roadmapFile, - id: roadmapPathToId(roadmapFile.file), - })); - - return filteredRoadmaps.sort( - (a, b) => a.frontmatter.order - b.frontmatter.order, - ); -} - -export async function getRoadmapById(id: string): Promise { - const roadmapFilesMap: Record = - import.meta.glob('/src/data/roadmaps/*/*.md', { - eager: true, - }); - - const roadmapFile = Object.values(roadmapFilesMap).find((roadmapFile) => { - return roadmapPathToId(roadmapFile.file) === id; - }); - - if (!roadmapFile) { - throw new Error(`Roadmap with ID ${id} not found`); - } - - return { - ...roadmapFile, - id: roadmapPathToId(roadmapFile.file), - }; -} - -export async function getRoadmapsByIds( - ids: string[], -): Promise { - if (!ids?.length) { - return []; - } - - return Promise.all(ids.map((id) => getRoadmapById(id))); -} - -export async function getRoadmapFaqsById(roadmapId: string): Promise { - const { faqs } = await import( - `../data/roadmaps/${roadmapId}/faqs.astro` - ).catch(() => ({})); - - return faqs || []; -} - export async function getResourceMeta( resourceType: ResourceType, resourceId: string, diff --git a/src/pages/404.astro b/src/pages/404.astro index 9e4261b86..553778cce 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -1,10 +1,13 @@ --- import Icon from '../components/AstroIcon.astro'; import BaseLayout from '../layouts/BaseLayout.astro'; -import { getRoadmapIds } from '../lib/roadmap'; +import { listOfficialRoadmaps } from '../queries/official-roadmap'; -const roadmapIds = await getRoadmapIds(); -const legacyRoadmapUrls = [...roadmapIds.map((id) => `/${id}/`), '/roadmaps/']; +const roadmapIds = await listOfficialRoadmaps(); +const legacyRoadmapUrls = [ + ...roadmapIds.map((roadmap) => `/${roadmap.slug}/`), + '/roadmaps/', +]; --- @@ -18,20 +21,26 @@ const legacyRoadmapUrls = [...roadmapIds.map((id) => `/${id}/`), '/roadmaps/'];
-
+
diff --git a/src/pages/[roadmapId].json.ts b/src/pages/[roadmapId].json.ts index c5720a5c4..87a9fb935 100644 --- a/src/pages/[roadmapId].json.ts +++ b/src/pages/[roadmapId].json.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import fs from 'node:fs'; import matter from 'gray-matter'; import { fileURLToPath } from 'node:url'; +import type { OfficialRoadmapDocument } from '../queries/official-roadmap'; export const prerender = false; @@ -10,9 +11,6 @@ export const prerender = false; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// hack to make it work. TODO: Fix -const projectRoot = path.resolve(__dirname, '../..').replace(/dist$/, ''); - type RoadmapJson = { _id: string; title: string; @@ -30,10 +28,16 @@ type RoadmapJson = { export async function fetchRoadmapJson( roadmapId: string, -): Promise { - const response = await fetch( - `https://roadmap.sh/api/v1-official-roadmap/${roadmapId}`, +): Promise { + const isDev = import.meta.env.DEV; + const baseUrl = new URL( + isDev ? 'http://localhost:8080' : 'https://roadmap.sh', ); + baseUrl.pathname = isDev + ? `/v1-official-roadmap/${roadmapId}` + : `/api/v1-official-roadmap/${roadmapId}`; + + const response = await fetch(String(baseUrl)); if (!response.ok) { throw new Error(`Failed to fetch roadmap json: ${response.statusText}`); @@ -56,48 +60,7 @@ export const GET: APIRoute = async function ({ params, request, props }) { }); } - // Construct the path to the markdown file - let roadmapFilePath = path.join( - projectRoot, - 'src', - 'data', - 'roadmaps', - roadmapId, - `${roadmapId}.md`, - ); - - let roadmapJsonPath = path.join( - projectRoot, - 'src', - 'data', - 'roadmaps', - roadmapId, - `${roadmapId}.json`, - ); - - if (!fs.existsSync(roadmapFilePath)) { - return new Response(JSON.stringify({ message: 'Roadmap not found' }), { - status: 404, - }); - } - - // Read and parse the markdown file - const fileContent = fs.readFileSync(roadmapFilePath, 'utf-8'); - const { data: frontmatter, content } = matter(fileContent); - - if (frontmatter.renderer !== 'editor') { - const roadmapJson = JSON.parse(fs.readFileSync(roadmapJsonPath, 'utf-8')); - - return new Response(JSON.stringify(roadmapJson), { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - const roadmapJson = await fetchRoadmapJson(roadmapId); - return new Response(JSON.stringify(roadmapJson), { status: 200, headers: { diff --git a/src/pages/[roadmapId]/ai.astro b/src/pages/[roadmapId]/ai.astro deleted file mode 100644 index 75e8c2215..000000000 --- a/src/pages/[roadmapId]/ai.astro +++ /dev/null @@ -1,43 +0,0 @@ ---- -import { CheckSubscriptionVerification } from '../../components/Billing/CheckSubscriptionVerification'; -import { RoadmapAIChat } from '../../components/RoadmapAIChat/RoadmapAIChat'; -import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; -import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; -import { getRoadmapById, getRoadmapIds } from '../../lib/roadmap'; - -type Props = { - roadmapId: string; -}; - -export const prerender = false; - -export async function getStaticPaths() { - const roadmapIds = await getRoadmapIds(); - - return roadmapIds.map((roadmapId) => ({ - params: { roadmapId }, - })); -} - -const { roadmapId } = Astro.params as Props; - -const roadmapDetail = await getRoadmapById(roadmapId); - -const canonicalUrl = `https://roadmap.sh/${roadmapId}/ai`; -const roadmapBriefTitle = roadmapDetail.frontmatter.briefTitle; ---- - - - - - - - diff --git a/src/pages/[roadmapId]/courses.astro b/src/pages/[roadmapId]/courses.astro index 227514a3d..6570657db 100644 --- a/src/pages/[roadmapId]/courses.astro +++ b/src/pages/[roadmapId]/courses.astro @@ -2,35 +2,26 @@ import RoadmapHeader from '../../components/RoadmapHeader.astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; import { getOpenGraphImageUrl } from '../../lib/open-graph'; -import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; -import CourseStep from '../../components/courses/CourseStep.astro'; -import Milestone from '../../components/courses/Milestone.astro'; import { getProjectsByRoadmapId } from '../../lib/project'; +import { officialRoadmapDetails } from '../../queries/official-roadmap'; -export const prerender = true; - -export async function getStaticPaths() { - const roadmapIds = await getRoadmapIds(); - - return roadmapIds.map((roadmapId) => ({ - params: { roadmapId }, - })); -} +export const prerender = false; interface Params extends Record { roadmapId: string; } const { roadmapId } = Astro.params as Params; -const roadmapFile = await import( - `../../data/roadmaps/${roadmapId}/${roadmapId}.md` -); - -const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter; +const roadmapData = await officialRoadmapDetails(roadmapId); +if (!roadmapData) { + Astro.response.status = 404; + Astro.response.statusText = 'Not found'; + return Astro.rewrite('/404'); +} // update og for projects const ogImageUrl = - roadmapData?.seo?.ogImageUrl || + roadmapData?.openGraph?.image || getOpenGraphImageUrl({ group: 'roadmap', resourceId: roadmapId, @@ -43,23 +34,23 @@ const descriptionNoun: Record = { 'Product Manager': 'Product Management', }; -const title = `${roadmapData.briefTitle} Courses`; -const description = `Premium courses to help you master ${descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle}`; +const title = `${roadmapData?.title.card} Courses`; +const description = `Premium courses to help you master ${descriptionNoun[roadmapData?.title.card] || roadmapData?.title.card}`; -const seoTitle = `${roadmapData.briefTitle} Courses`; +const seoTitle = `${roadmapData?.title.card} Courses`; const nounTitle = - descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle; + descriptionNoun[roadmapData.title.card] || roadmapData.title.card; const seoDescription = `Seeking ${nounTitle.toLowerCase()} courses to enhance your skills? Explore our top free and paid courses to help you become a ${nounTitle} expert!`; const projects = await getProjectsByRoadmapId(roadmapId); -const courses = roadmapData.courses || []; +const courses = roadmapData?.courses || []; --- ({ - params: { roadmapId }, - })); -} +export const prerender = false; interface Params extends Record { roadmapId: string; } const { roadmapId } = Astro.params as Params; -const roadmapFile = await import( - `../../data/roadmaps/${roadmapId}/${roadmapId}.md` -); -const { faqs: roadmapFAQs = [] } = await import( - `../../data/roadmaps/${roadmapId}/faqs.astro` -); -const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter; +const roadmapData = await officialRoadmapDetails(roadmapId); +if (!roadmapData) { + Astro.response.status = 404; + Astro.response.statusText = 'Not found'; + return Astro.rewrite('/404'); +} let jsonLdSchema = []; -if (roadmapData.schema) { - const roadmapSchema = roadmapData.schema; - jsonLdSchema.push( - generateArticleSchema({ - url: `https://roadmap.sh/${roadmapId}`, - headline: roadmapSchema.headline, - description: roadmapSchema.description, - datePublished: roadmapSchema.datePublished, - dateModified: roadmapSchema.dateModified, - imageUrl: roadmapSchema.imageUrl, - }), - ); -} +const datePublished = DateTime.fromJSDate( + new Date(roadmapData?.createdAt), +).toFormat('yyyy-MM-dd'); +const dateModified = DateTime.fromJSDate( + new Date(roadmapData?.updatedAt), +).toFormat('yyyy-MM-dd'); -if (roadmapFAQs.length) { - jsonLdSchema.push(generateFAQSchema(roadmapFAQs as unknown as FAQType[])); -} +const baseUrl = import.meta.env.DEV + ? `http://localhost:8080` + : `https://roadmap.sh`; + +jsonLdSchema.push( + generateArticleSchema({ + url: `https://roadmap.sh/${roadmapId}`, + headline: roadmapData?.seo?.title || roadmapData?.title?.page, + description: roadmapData?.description, + datePublished, + dateModified, + imageUrl: `${baseUrl}/roadmaps/${roadmapId}.png`, + }), +); const ogImageUrl = - roadmapData?.seo?.ogImageUrl || + roadmapData?.openGraph?.image || getOpenGraphImageUrl({ group: 'roadmap', resourceId: roadmapId, }); -const question = roadmapData?.question; -const note = roadmapData.note; +const question = roadmapData?.questions?.find( + (question) => question.type === 'main', +); +const faqs = + roadmapData?.questions?.filter((question) => question.type === 'faq') || []; +if (faqs.length) { + jsonLdSchema.push(generateFAQSchema(faqs)); +} + const projects = await getProjectsByRoadmapId(roadmapId); const courses = roadmapData.courses || []; --- -
@@ -142,33 +138,23 @@ const courses = roadmapData.courses || []; - { - roadmapData?.renderer === 'editor' ? ( - - ) : ( - - ) - } +
@@ -180,9 +166,8 @@ const courses = roadmapData.courses || []; ) } - - - + +
diff --git a/src/pages/[roadmapId]/projects.astro b/src/pages/[roadmapId]/projects.astro index c434cc4bc..ccff751d7 100644 --- a/src/pages/[roadmapId]/projects.astro +++ b/src/pages/[roadmapId]/projects.astro @@ -5,33 +5,26 @@ import { ProjectsList } from '../../components/Projects/ProjectsList'; import BaseLayout from '../../layouts/BaseLayout.astro'; import { getProjectsByRoadmapId } from '../../lib/project'; import { getOpenGraphImageUrl } from '../../lib/open-graph'; -import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; import { projectApi } from '../../api/project'; +import { officialRoadmapDetails } from '../../queries/official-roadmap'; -export const prerender = true; - -export async function getStaticPaths() { - const roadmapIds = await getRoadmapIds(); - - return roadmapIds.map((roadmapId) => ({ - params: { roadmapId }, - })); -} +export const prerender = false; interface Params extends Record { roadmapId: string; } const { roadmapId } = Astro.params as Params; -const roadmapFile = await import( - `../../data/roadmaps/${roadmapId}/${roadmapId}.md` -); - -const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter; +const roadmapData = await officialRoadmapDetails(roadmapId); +if (!roadmapData) { + Astro.response.status = 404; + Astro.response.statusText = 'Not found'; + return Astro.rewrite('/404'); +} // update og for projects const ogImageUrl = - roadmapData?.seo?.ogImageUrl || + roadmapData?.openGraph?.image || getOpenGraphImageUrl({ group: 'roadmap', resourceId: roadmapId, @@ -44,13 +37,13 @@ const descriptionNoun: Record = { 'Product Manager': 'Product Management', }; -const title = `${roadmapData.briefTitle} Projects`; -const description = `Project ideas to take you from beginner to advanced in ${descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle}`; +const title = `${roadmapData.title.card} Projects`; +const description = `Project ideas to take you from beginner to advanced in ${descriptionNoun[roadmapData.title.card] || roadmapData.title.card}`; // `Seeking backend projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!` -const seoTitle = `${roadmapData.briefTitle} Projects`; +const seoTitle = `${roadmapData.title.card} Projects`; const nounTitle = - descriptionNoun[roadmapData?.briefTitle] || roadmapData.briefTitle; + descriptionNoun[roadmapData?.title.card] || roadmapData.title.card; const seoDescription = `Seeking ${nounTitle.toLowerCase()} projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`; const projects = await getProjectsByRoadmapId(roadmapId); @@ -65,7 +58,7 @@ const { response: userCounts } = permalink={`/${roadmapId}/projects`} title={seoTitle} description={seoDescription} - briefTitle={roadmapData.briefTitle} + briefTitle={roadmapData.title.card} ogImageUrl={ogImageUrl} keywords={roadmapData.seo.keywords} noIndex={projects.length === 0} @@ -76,13 +69,9 @@ const { response: userCounts } = ({ - params: { roadmapId }, - })); -} +export const prerender = false; interface Params extends Record { roadmapId: string; } const { roadmapId } = Astro.params as Params; -const roadmapFile = await import( - `../../data/roadmaps/${roadmapId}/${roadmapId}.md` -); -const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter; +const roadmapData = await officialRoadmapDetails(roadmapId); +if (!roadmapData) { + Astro.response.status = 404; + Astro.response.statusText = 'Not found'; + return Astro.rewrite('/404'); +} const ogImageUrl = - roadmapData?.seo?.ogImageUrl || + roadmapData?.openGraph?.image || getOpenGraphImageUrl({ group: 'roadmap', resourceId: roadmapId, @@ -35,8 +28,8 @@ const ogImageUrl = -
- { - roadmapData?.renderer === 'editor' ? ( - - ) : ( - - ) - } +
+
diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index 08fdffa52..88af1687a 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -1,56 +1,53 @@ --- +import { DateTime } from 'luxon'; import { DashboardPage } from '../components/Dashboard/DashboardPage'; import BaseLayout from '../layouts/BaseLayout.astro'; import { getAllBestPractices } from '../lib/best-practice'; import { getAllQuestionGroups } from '../lib/question-group'; -import { getRoadmapsByTag } from '../lib/roadmap'; import { getAllVideos } from '../lib/video'; import { listOfficialGuides } from '../queries/official-guide'; +import { + isNewRoadmap, + listOfficialRoadmaps, +} from '../queries/official-roadmap'; +import type { BuiltInRoadmap } from '../components/Dashboard/PersonalDashboard'; -const roleRoadmaps = await getRoadmapsByTag('role-roadmap'); -const skillRoadmaps = await getRoadmapsByTag('skill-roadmap'); +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 questionGroups = await getAllQuestionGroups(); const guides = await listOfficialGuides(); const videos = await getAllVideos(); -const enrichedRoleRoadmaps = roleRoadmaps - .filter((roadmapItem) => !roadmapItem.frontmatter.isHidden) - .map((roadmap) => { - const { frontmatter } = roadmap; - - return { - id: roadmap.id, - url: `/${roadmap.id}`, - title: frontmatter.briefTitle, - description: frontmatter.briefDescription, - relatedRoadmapIds: frontmatter.relatedRoadmaps, - renderer: frontmatter.renderer, - isNew: frontmatter.isNew, - metadata: { - tags: frontmatter.tags, - }, - }; - }); -const enrichedSkillRoadmaps = skillRoadmaps - .filter((roadmapItem) => !roadmapItem.frontmatter.isHidden) - .map((roadmap) => { - const { frontmatter } = roadmap; - - return { - id: roadmap.id, - url: `/${roadmap.id}`, - title: - frontmatter.briefTitle === 'Go' ? 'Go Roadmap' : frontmatter.briefTitle, - description: frontmatter.briefDescription, - relatedRoadmapIds: frontmatter.relatedRoadmaps, - renderer: frontmatter.renderer, - isNew: frontmatter.isNew, - metadata: { - tags: frontmatter.tags, - }, - }; - }); +const enrichedRoleRoadmaps: BuiltInRoadmap[] = roleRoadmaps.map((roadmap) => { + return { + id: roadmap.slug, + url: `/${roadmap.slug}`, + title: roadmap.title.card, + description: roadmap.description, + relatedRoadmapIds: roadmap.relatedRoadmaps, + renderer: 'editor', + isNew: isNewRoadmap(roadmap.createdAt), + metadata: { + tags: ['role-roadmap'], + }, + }; +}); +const enrichedSkillRoadmaps: BuiltInRoadmap[] = skillRoadmaps.map((roadmap) => { + return { + id: roadmap.slug, + url: `/${roadmap.slug}`, + title: roadmap.title.card === 'Go' ? 'Go Roadmap' : roadmap.title.card, + description: roadmap.description, + relatedRoadmapIds: roadmap.relatedRoadmaps, + renderer: 'editor', + isNew: isNewRoadmap(roadmap.createdAt), + metadata: { + tags: ['skill-roadmap'], + }, + }; +}); const enrichedBestPractices = bestPractices.map((bestPractice) => { const { frontmatter } = bestPractice; diff --git a/src/pages/index.astro b/src/pages/index.astro index bbbd2aab8..658bd2677 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,4 +1,5 @@ --- +import { DateTime } from 'luxon'; import ChangelogBanner from '../components/ChangelogBanner.astro'; import { FeaturedGuideList } from '../components/FeaturedGuides/FeaturedGuideList'; import FeaturedItems from '../components/FeaturedItems/FeaturedItems.astro'; @@ -6,12 +7,16 @@ import { FeaturedVideoList } from '../components/FeaturedVideos/FeaturedVideoLis import HeroSection from '../components/HeroSection/HeroSection.astro'; import BaseLayout from '../layouts/BaseLayout.astro'; import { getAllBestPractices } from '../lib/best-practice'; -import { getRoadmapsByTag } from '../lib/roadmap'; import { getAllVideos } from '../lib/video'; import { listOfficialGuides } from '../queries/official-guide'; +import { + isNewRoadmap, + listOfficialRoadmaps, +} from '../queries/official-roadmap'; -const roleRoadmaps = await getRoadmapsByTag('role-roadmap'); -const skillRoadmaps = await getRoadmapsByTag('skill-roadmap'); +const roadmaps = await listOfficialRoadmaps(); +const roleRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'role'); +const skillRoadmaps = roadmaps.filter((roadmap) => roadmap.type === 'skill'); const bestPractices = await getAllBestPractices(); export const projectGroups = [ @@ -47,33 +52,32 @@ const videos = await getAllVideos(); !roadmapItem.frontmatter.isHidden) - .map((roadmapItem) => ({ - text: roadmapItem.frontmatter.briefTitle, - url: `/${roadmapItem.id}`, - isNew: roadmapItem.frontmatter.isNew, - isUpcoming: roadmapItem.frontmatter.isUpcoming, - }))} + featuredItems={roleRoadmaps.map((roadmapItem) => { + const isNew = isNewRoadmap(roadmapItem.createdAt); + + return { + text: roadmapItem.title.card, + url: `/${roadmapItem.slug}`, + isNew, + }; + })} showCreateRoadmap={true} /> !roadmapItem.frontmatter.isHidden) - .map((roadmapItem) => ({ + featuredItems={skillRoadmaps.map((roadmapItem) => { + const isNew = isNewRoadmap(roadmapItem.createdAt); + + return { text: - roadmapItem.frontmatter.briefTitle === 'Go' + roadmapItem.title.card === 'Go' ? 'Go Roadmap' - : roadmapItem.frontmatter.briefTitle.replace( - 'Software Design', - 'Design', - ), - url: `/${roadmapItem.id}`, - isNew: roadmapItem.frontmatter.isNew, - isUpcoming: roadmapItem.frontmatter.isUpcoming, - }))} + : roadmapItem.title.card.replace('Software Design', 'Design'), + url: `/${roadmapItem.slug}`, + isNew, + }; + })} showCreateRoadmap={true} /> diff --git a/src/pages/pages.json.ts b/src/pages/pages.json.ts index 1819f45b5..eec165088 100644 --- a/src/pages/pages.json.ts +++ b/src/pages/pages.json.ts @@ -1,5 +1,4 @@ import { getAllBestPractices } from '../lib/best-practice'; -import { getRoadmapsByTag } from '../lib/roadmap'; import { getAllVideos } from '../lib/video'; import { getAllQuestionGroups } from '../lib/question-group'; import { getAllProjects } from '../lib/project'; @@ -7,6 +6,7 @@ import { listOfficialAuthors, listOfficialGuides, } from '../queries/official-guide'; +import { listOfficialRoadmaps } from '../queries/official-roadmap'; // Add utility to fetch beginner roadmap file IDs function getBeginnerRoadmapIds() { @@ -25,40 +25,49 @@ export async function GET() { const authors = await listOfficialAuthors(); const videos = await getAllVideos(); const questionGroups = await getAllQuestionGroups(); - const roadmaps = await getRoadmapsByTag('roadmap'); + const roadmaps = await listOfficialRoadmaps(); + const bestPractices = await getAllBestPractices(); const projects = await getAllProjects(); // Transform main roadmaps into page objects first so that we can reuse their meta for beginner variants - const roadmapPages = roadmaps.map((roadmap) => ({ - id: roadmap.id, - url: `/${roadmap.id}`, - title: roadmap.frontmatter.briefTitle, - shortTitle: roadmap.frontmatter.title, - description: roadmap.frontmatter.briefDescription, - group: 'Roadmaps', - metadata: { - tags: roadmap.frontmatter.tags, - }, - renderer: roadmap?.frontmatter?.renderer || 'balsamiq', - })); + const roadmapPages = roadmaps + .map((roadmap) => { + const isBeginner = roadmap.slug.endsWith('-beginner'); + if (!isBeginner) { + return { + id: roadmap.slug, + url: `/${roadmap.slug}`, + title: roadmap.title.card, + shortTitle: roadmap.title.card, + description: roadmap.description, + group: 'Roadmaps', + metadata: { + tags: + roadmap.type === 'role' ? ['role-roadmap'] : ['skill-roadmap'], + }, + renderer: 'editor', + }; + } - // Generate beginner roadmap page objects - const beginnerRoadmapPages = getBeginnerRoadmapIds() - .map((beginnerId) => { - const parentId = beginnerId.replace('-beginner', ''); - const parentMeta = roadmapPages.find((page) => page.id === parentId); + const parentSlug = roadmap.slug.replace('-beginner', ''); + const parentMeta = roadmaps.find((r) => r.slug === parentSlug); if (!parentMeta) { return null; } return { - ...parentMeta, - id: beginnerId, - url: `/${parentId}?r=${beginnerId}`, - title: `${parentMeta.title} Beginner`, - shortTitle: `${parentMeta.shortTitle} Beginner`, + id: roadmap.slug, + url: `/${parentSlug}?r=${roadmap.slug}`, + title: `${parentMeta.title.page} Beginner`, + shortTitle: `${parentMeta.title.page} Beginner`, + description: parentMeta.description, + group: 'Roadmaps', + metadata: { + tags: ['beginner-roadmap'], + }, + renderer: 'editor', }; }) .filter(Boolean); @@ -66,7 +75,6 @@ export async function GET() { return new Response( JSON.stringify([ ...roadmapPages, - ...beginnerRoadmapPages, ...bestPractices.map((bestPractice) => ({ id: bestPractice.id, url: `/best-practices/${bestPractice.id}`, diff --git a/src/pages/projects/[projectId]/index.astro b/src/pages/projects/[projectId]/index.astro index 4fe963e04..038044ee2 100644 --- a/src/pages/projects/[projectId]/index.astro +++ b/src/pages/projects/[projectId]/index.astro @@ -11,17 +11,7 @@ import { ProjectStepper } from '../../../components/Projects/StatusStepper/Proje import { ProjectTrackingActions } from '../../../components/Projects/StatusStepper/ProjectTrackingActions'; import { ProjectTabs } from '../../../components/Projects/ProjectTabs'; -export const prerender = true; - -export async function getStaticPaths() { - const projects = await getAllProjects(); - - return projects - .map((project) => project.id) - .map((projectId) => ({ - params: { projectId }, - })); -} +export const prerender = false; interface Params extends Record { projectId: string; @@ -30,6 +20,12 @@ interface Params extends Record { const { projectId } = Astro.params as Params; const project = await getProjectById(projectId); +if (!project) { + Astro.response.status = 404; + Astro.response.statusText = 'Not found'; + return Astro.rewrite('/404'); +} + const projectData = project.frontmatter as ProjectFrontmatter; let jsonLdSchema: any[] = []; diff --git a/src/pages/projects/[projectId]/solutions.astro b/src/pages/projects/[projectId]/solutions.astro index 344f4a0a0..d533956db 100644 --- a/src/pages/projects/[projectId]/solutions.astro +++ b/src/pages/projects/[projectId]/solutions.astro @@ -9,18 +9,7 @@ import { ProjectTabs } from '../../../components/Projects/ProjectTabs'; import { ListProjectSolutions } from '../../../components/Projects/ListProjectSolutions'; import { ProjectSolutionModal } from '../../../components/Projects/ProjectSolutionModal'; -export const prerender = true; - -export async function getStaticPaths() { - const projects = await getAllProjects(); - - return projects - .filter((project) => !(project?.frontmatter?.hasNoSubmission || false)) - .map((project) => project.id) - .map((projectId) => ({ - params: { projectId }, - })); -} +export const prerender = false; interface Params extends Record { projectId: string; @@ -29,6 +18,12 @@ interface Params extends Record { const { projectId } = Astro.params as Params; const project = await getProjectById(projectId); +if (!project) { + Astro.response.status = 404; + Astro.response.statusText = 'Not found'; + return Astro.rewrite('/404'); +} + const projectData = project.frontmatter as ProjectFrontmatter; let jsonLdSchema: any[] = []; diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index 83dc0042f..8e52067b7 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -1,23 +1,29 @@ --- import BaseLayout from '../../layouts/BaseLayout.astro'; import { getRoadmapsProjects } from '../../lib/project'; -import { getRoadmapsByIds } from '../../lib/roadmap'; import { ProjectsPageHeader } from '../../components/Projects/ProjectsPageHeader'; import { ProjectsPage } from '../../components/Projects/ProjectsPage'; import { projectApi } from '../../api/project'; +import { listOfficialRoadmaps } from '../../queries/official-roadmap'; + +export const prerender = false; const roadmapProjects = await getRoadmapsProjects(); const allRoadmapIds = Object.keys(roadmapProjects); -const allRoadmaps = await getRoadmapsByIds(allRoadmapIds); +const roadmaps = await listOfficialRoadmaps(); +const allRoadmaps = roadmaps.filter((roadmap) => + allRoadmapIds.includes(roadmap.slug), +); + const enrichedRoadmaps = allRoadmaps.map((roadmap) => { - const projects = (roadmapProjects[roadmap.id] || []).sort((a, b) => { + const projects = (roadmapProjects[roadmap.slug] || []).sort((a, b) => { return a.frontmatter.sort - b.frontmatter.sort; }); return { - id: roadmap.id, - title: roadmap.frontmatter.briefTitle, + id: roadmap.slug, + title: roadmap.title.card, projects, }; }); @@ -42,5 +48,5 @@ const { response: userCounts } = userCounts={userCounts || {}} client:load /> -
+
diff --git a/src/pages/roadmaps.astro b/src/pages/roadmaps.astro index c0f4db6be..a5b202f95 100644 --- a/src/pages/roadmaps.astro +++ b/src/pages/roadmaps.astro @@ -1,10 +1,7 @@ --- import { RoadmapsPage } from '../components/Roadmaps/RoadmapsPage'; import { RoadmapsPageHeader } from '../components/Roadmaps/RoadmapsPageHeader'; -import GridItem from '../components/GridItem.astro'; -import SimplePageHeader from '../components/SimplePageHeader.astro'; import BaseLayout from '../layouts/BaseLayout.astro'; -import { getRoadmapsByTag } from '../lib/roadmap'; import ChangelogBanner from '../components/ChangelogBanner.astro'; --- diff --git a/src/queries/official-roadmap.ts b/src/queries/official-roadmap.ts index 349abb946..15dc8f2f0 100644 --- a/src/queries/official-roadmap.ts +++ b/src/queries/official-roadmap.ts @@ -1,24 +1,137 @@ import { queryOptions } from '@tanstack/react-query'; -import { httpGet } from '../lib/query-http'; +import { FetchError, httpGet } from '../lib/query-http'; import type { Node, Edge } from '@roadmapsh/editor'; +import { DateTime } from 'luxon'; -export interface OfficialRoadmapDocument { +export const allowedOfficialRoadmapType = ['skill', 'role'] as const; +export type AllowedOfficialRoadmapType = + (typeof allowedOfficialRoadmapType)[number]; + +export const allowedOfficialRoadmapQuestionType = ['faq', 'main'] as const; +export type AllowedOfficialRoadmapQuestionType = + (typeof allowedOfficialRoadmapQuestionType)[number]; + +export type OfficialRoadmapQuestion = { + _id: string; + type: AllowedOfficialRoadmapQuestionType; + title: string; + // Tiptap JSON Content + description: any; +}; + +export type OfficialRoadmapCourse = { _id: string; title: string; - description?: string; + description: string; + link: string; + instructor: { + name: string; + image: string; + title: string; + }; + features: string[]; +}; + +export interface OfficialRoadmapDocument { + _id?: string; + order: number; + + title: { + card: string; + page: string; + }; + description: string; + slug: string; - nodes: Node[]; - edges: Edge[]; + nodes: any[]; + edges: any[]; + + draft: { + nodes: any[]; + edges: any[]; + }; + + seo: { + title: string; + description: string; + keywords: string[]; + }; + openGraph?: { + image?: string; + }; + partner?: { + description: string; + linkText: string; + link: string; + }; + type: AllowedOfficialRoadmapType; + dimensions?: { + height: number; + width: number; + }; + + questions?: OfficialRoadmapQuestion[]; + relatedRoadmaps?: string[]; + courses?: string[]; createdAt: Date; updatedAt: Date; } +export type OfficialRoadmapWithCourses = Omit< + OfficialRoadmapDocument, + 'courses' +> & { + courses: OfficialRoadmapCourse[]; +}; + export function officialRoadmapOptions(slug: string) { return queryOptions({ queryKey: ['official-roadmap', slug], queryFn: () => { - return httpGet(`/v1-official-roadmap/${slug}`); + return httpGet( + `/v1-official-roadmap/${slug}`, + ); }, }); } + +export async function officialRoadmapDetails(roadmapSlug: string) { + try { + const roadmap = await httpGet( + `/v1-official-roadmap/${roadmapSlug}`, + ); + + return roadmap; + } catch (error) { + if (FetchError.isFetchError(error) && error.status === 404) { + return null; + } + + throw error; + } +} + +export async function listOfficialRoadmaps() { + try { + const roadmaps = await httpGet( + `/v1-list-official-roadmaps`, + ); + + return roadmaps; + } catch (error) { + if (FetchError.isFetchError(error) && error.status === 404) { + return []; + } + + throw error; + } +} + +export function isNewRoadmap(createdAt: Date) { + return ( + createdAt && + DateTime.now().diff(DateTime.fromJSDate(new Date(createdAt)), 'days').days < + 45 + ); +}