feat: migrate projects

feat/collection
Arik Chakma 2 months ago
parent 0aca915e21
commit 2333694e2c
  1. 3
      src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
  2. 4
      src/components/Guide/GuideContent.astro
  3. 14
      src/components/GuideHeader.astro
  4. 5
      src/components/Projects/ListProjectSolutions.tsx
  5. 8
      src/components/Projects/ProjectCard.tsx
  6. 29
      src/components/Projects/ProjectsList.tsx
  7. 16
      src/components/Projects/ProjectsPage.tsx
  8. 6
      src/components/Questions/QuestionGuide.astro
  9. 9
      src/components/VideoHeader.astro
  10. 2
      src/content/config.ts
  11. 28
      src/content/project.ts
  12. 0
      src/content/projects/accessible-form-ui.md
  13. 0
      src/content/projects/accordion.md
  14. 0
      src/content/projects/basic-dockerfile.md
  15. 0
      src/content/projects/basic-html-website.md
  16. 0
      src/content/projects/blogging-platform-api.md
  17. 0
      src/content/projects/broadcast-server.md
  18. 0
      src/content/projects/caching-server.md
  19. 0
      src/content/projects/changelog-component.md
  20. 0
      src/content/projects/cookie-consent.md
  21. 0
      src/content/projects/custom-dropdown.md
  22. 0
      src/content/projects/database-backup-utility.md
  23. 0
      src/content/projects/datepicker-ui.md
  24. 0
      src/content/projects/ecommerce-api.md
  25. 0
      src/content/projects/expense-tracker-api.md
  26. 0
      src/content/projects/expense-tracker.md
  27. 0
      src/content/projects/fitness-workout-tracker.md
  28. 0
      src/content/projects/github-random-repo.md
  29. 0
      src/content/projects/github-user-activity.md
  30. 0
      src/content/projects/image-grid.md
  31. 0
      src/content/projects/image-processing-service.md
  32. 0
      src/content/projects/log-archive-tool.md
  33. 0
      src/content/projects/markdown-note-taking-app.md
  34. 0
      src/content/projects/movie-reservation-system.md
  35. 0
      src/content/projects/number-guessing-game.md
  36. 0
      src/content/projects/personal-blog.md
  37. 0
      src/content/projects/portfolio-website.md
  38. 0
      src/content/projects/realtime-leaderboard-system.md
  39. 0
      src/content/projects/reddit-client.md
  40. 0
      src/content/projects/restricted-textarea.md
  41. 0
      src/content/projects/scalable-ecommerce-platform.md
  42. 0
      src/content/projects/simple-tabs.md
  43. 0
      src/content/projects/single-page-cv.md
  44. 0
      src/content/projects/task-tracker-js.md
  45. 0
      src/content/projects/task-tracker.md
  46. 10
      src/content/projects/temperature-converter.md
  47. 0
      src/content/projects/testimonial-cards.md
  48. 0
      src/content/projects/todo-list-api.md
  49. 0
      src/content/projects/tooltip-ui.md
  50. 0
      src/content/projects/unit-converter.md
  51. 0
      src/content/projects/url-shortening-service.md
  52. 0
      src/content/projects/weather-api-wrapper-service.md
  53. 68
      src/lib/project.ts
  54. 4
      src/lib/video.ts
  55. 2
      src/pages/[roadmapId]/projects.astro
  56. 2
      src/pages/authors/[authorId].astro
  57. 2
      src/pages/index.astro
  58. 16
      src/pages/pages.json.ts
  59. 19
      src/pages/projects/[projectId]/index.astro
  60. 10
      src/pages/projects/[projectId]/solutions.astro
  61. 4
      src/pages/projects/index.astro
  62. 2
      src/pages/questions/[questionGroupId].astro

@ -159,7 +159,8 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
{hideRenderer && (
<EmptyRoadmap
roadmapId={roadmapId}
canManage={roadmap.canManage}
// @ts-ignore
canManage={roadmap?.canManage}
className="grow"
/>
)}

@ -40,7 +40,7 @@ const { data: guideFrontmatter, author } = guide;
</h1>
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a
href={`/authors/${author.id}`}
href={`/authors/${author.slug}`}
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
>
<img
@ -53,7 +53,7 @@ const { data: guideFrontmatter, author } = guide;
<span class='mx-2 hidden sm:inline'>&middot;</span>
<a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`}
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.slug}.md`}
target='_blank'
>
Improve this Guide

@ -7,7 +7,7 @@ export interface Props {
}
const { guide } = Astro.props;
const { frontmatter, author } = guide;
const { data: frontmatter, author } = guide;
return undefined;
---
@ -18,18 +18,18 @@ return undefined;
class='hidden items-center justify-start text-gray-400 sm:flex sm:justify-center'
>
{
author?.frontmatter && (
author?.data && (
<>
<a
href={`/authors/${author.id}`}
href={`/authors/${author.slug}`}
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline'
>
<img
alt={author.frontmatter.name}
src={author.frontmatter.imageUrl}
alt={author.data.name}
src={author.data.imageUrl}
class='mr-2 inline h-5 w-5 rounded-full'
/>
{author.frontmatter.name}
{author.data.name}
</a>
<span class='mx-1.5'>&middot;</span>
</>
@ -39,7 +39,7 @@ return undefined;
<span class='mx-1.5'>&middot;</span>
<a
class='text-blue-400 hover:text-blue-500 hover:underline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`}
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.slug}.md`}
target='_blank'>Improve this Guide</a
>
</p>

@ -14,8 +14,7 @@ import { showLoginPopup } from '../../lib/popup';
import { VoteButton } from './VoteButton.tsx';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import { SelectLanguages } from './SelectLanguages.tsx';
import type { ProjectFrontmatter } from '../../lib/project.ts';
import { ProjectSolutionModal } from './ProjectSolutionModal.tsx';
import type { ProjectFileType } from '../../lib/project.ts';
export interface ProjectStatusDocument {
_id?: string;
@ -65,7 +64,7 @@ type PageState = {
};
type ListProjectSolutionsProps = {
project: ProjectFrontmatter;
project: ProjectFileType['data'];
projectId: string;
};

@ -1,10 +1,8 @@
import { Badge } from '../Badge.tsx';
import type {
ProjectDifficultyType,
ProjectFileType,
} from '../../lib/project.ts';
import type { ProjectFileType } from '../../lib/project.ts';
import { Users } from 'lucide-react';
import { formatCommaNumber } from '../../lib/number.ts';
import type { ProjectDifficultyType } from '../../content/project.ts';
type ProjectCardProps = {
project: ProjectFileType;
@ -20,7 +18,7 @@ const badgeVariants: Record<ProjectDifficultyType, string> = {
export function ProjectCard(props: ProjectCardProps) {
const { project, userCount = 0 } = props;
const { frontmatter, id } = project;
const { data: frontmatter, slug: id } = project;
return (
<a

@ -2,16 +2,16 @@ import { ProjectCard } from './ProjectCard.tsx';
import { HeartHandshake, Trash2 } from 'lucide-react';
import { cn } from '../../lib/classname.ts';
import { useMemo, useState } from 'react';
import {
projectDifficulties,
type ProjectDifficultyType,
type ProjectFileType,
} from '../../lib/project.ts';
import { type ProjectFileType } from '../../lib/project.ts';
import {
deleteUrlParam,
getUrlParams,
setUrlParams,
} from '../../lib/browser.ts';
import {
projectDifficulties,
type ProjectDifficultyType,
} from '../../content/project.ts';
type DifficultyButtonProps = {
difficulty: ProjectDifficultyType;
@ -56,7 +56,7 @@ export function ProjectsList(props: ProjectsListProps) {
const result = new Map<ProjectDifficultyType, ProjectFileType[]>();
for (const project of projects) {
const difficulty = project.frontmatter.difficulty;
const difficulty = project.data.difficulty;
if (!result.has(difficulty)) {
result.set(difficulty, []);
@ -78,6 +78,7 @@ export function ProjectsList(props: ProjectsListProps) {
<div className="flex flex-wrap gap-1">
{projectDifficulties.map((projectDifficulty) => (
<DifficultyButton
key={projectDifficulty}
onClick={() => {
setDifficulty(projectDifficulty);
setUrlParams({ difficulty: projectDifficulty });
@ -119,18 +120,24 @@ export function ProjectsList(props: ProjectsListProps) {
{matchingProjects
.sort((project) => {
return project.frontmatter.difficulty === 'beginner'
return project.data.difficulty === 'beginner'
? -1
: project.frontmatter.difficulty === 'intermediate'
: project.data.difficulty === 'intermediate'
? 0
: 1;
})
.sort((a, b) => {
return a.frontmatter.sort - b.frontmatter.sort;
return a.data.sort - b.data.sort;
})
.map((matchingProject) => {
const count = userCounts[matchingProject?.id] || 0;
return <ProjectCard project={matchingProject} userCount={count} />;
const count = userCounts[matchingProject?.slug] || 0;
return (
<ProjectCard
key={matchingProject.slug}
project={matchingProject}
userCount={count}
/>
);
})}
</div>
</div>

@ -7,11 +7,9 @@ import {
setUrlParams,
} from '../../lib/browser.ts';
import { CategoryFilterButton } from '../Roadmaps/CategoryFilterButton.tsx';
import {
projectDifficulties,
type ProjectFileType,
} from '../../lib/project.ts';
import { type ProjectFileType } from '../../lib/project.ts';
import { ProjectCard } from './ProjectCard.tsx';
import { projectDifficulties } from '../../content/project.ts';
type ProjectGroup = {
id: string;
@ -28,7 +26,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
const { roadmapsProjects, userCounts } = props;
const allUniqueProjectIds = new Set<string>(
roadmapsProjects.flatMap((group) =>
group.projects.map((project) => project.id),
group.projects.map((project) => project.slug),
),
);
const allUniqueProjects = useMemo(
@ -37,7 +35,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
.map((id) =>
roadmapsProjects
.flatMap((group) => group.projects)
.find((project) => project.id === id),
.find((project) => project.slug === id),
)
.filter(Boolean) as ProjectFileType[],
[allUniqueProjectIds],
@ -67,8 +65,8 @@ export function ProjectsPage(props: ProjectsPageProps) {
const sortedVisibleProjects = useMemo(
() =>
visibleProjects.sort((a, b) => {
const projectADifficulty = a?.frontmatter.difficulty || 'beginner';
const projectBDifficulty = b?.frontmatter.difficulty || 'beginner';
const projectADifficulty = a?.data.difficulty || 'beginner';
const projectBDifficulty = b?.data.difficulty || 'beginner';
return (
projectDifficulties.indexOf(projectADifficulty) -
projectDifficulties.indexOf(projectBDifficulty)
@ -189,7 +187,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
<ProjectCard
key={project.id}
project={project}
userCount={userCounts[project.id] || 0}
userCount={userCounts[project.slug] || 0}
/>
))}
</div>

@ -80,7 +80,7 @@ const { data: guideFrontmatter, author } = questionGroup;
author && (
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a
href={`/authors/${author?.id}`}
href={`/authors/${author?.slug}`}
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
>
<img
@ -93,7 +93,7 @@ const { data: guideFrontmatter, author } = questionGroup;
<span class='mx-2 hidden sm:inline'>&middot;</span>
<a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/question-groups/${questionGroup.id}`}
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/question-groups/${questionGroup.slug}`}
target='_blank'
>
Improve this Guide
@ -110,7 +110,7 @@ const { data: guideFrontmatter, author } = questionGroup;
</p>
<div class='mx-0 sm:-mb-32'>
<QuestionsList
groupId={questionGroup.id}
groupId={questionGroup.slug}
questions={questionGroup.questions}
client:load
/>

@ -1,6 +1,5 @@
---
import type { VideoFileType } from '../lib/video';
import YouTubeAlert from './YouTubeAlert.astro';
export interface Props {
video: VideoFileType;
@ -16,15 +15,15 @@ const { frontmatter, author } = video;
class='hidden items-center justify-start text-gray-400 sm:flex sm:justify-center'
>
<a
href={`/authors/${author.id}`}
href={`/authors/${author.slug}`}
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline'
>
<img
alt={author.frontmatter.name}
src={author.frontmatter.imageUrl}
alt={author.data.name}
src={author.data.imageUrl}
class='mr-2 inline h-5 w-5 rounded-full'
/>
{author.frontmatter.name}
{author.data.name}
</a>
<span class='mx-1.5'>&middot;</span>
<span class='capitalize'>Illustrated Video</span>

@ -1,9 +1,11 @@
import { authorCollection } from './author';
import { guideCollection } from './guide';
import { projectCollection } from './project';
import { questionGroupCollection } from './question-group';
export const collections = {
authors: authorCollection,
guides: guideCollection,
'question-groups': questionGroupCollection,
projects: projectCollection,
};

@ -0,0 +1,28 @@
import { defineCollection, z } from 'astro:content';
export const projectDifficulties = [
'beginner',
'intermediate',
'advanced',
] as const;
export type ProjectDifficultyType = (typeof projectDifficulties)[number];
export const projectCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
isNew: z.boolean(),
sort: z.number(),
difficulty: z.enum(projectDifficulties),
nature: z.string(),
skills: z.array(z.string()),
seo: z.object({
title: z.string(),
description: z.string(),
keywords: z.array(z.string()),
ogImageUrl: z.string().optional(),
}),
roadmapIds: z.array(z.string()),
}),
});

@ -11,14 +11,14 @@ skills:
- JavaScript
- DOM Manipulation
seo:
- title: Build a Temperature Converter with JavaScript
- description: Learn how to create an interactive temperature converter that converts between Celsius, Fahrenheit, and Kelvin using JavaScript.
- keywords:
title: Build a Temperature Converter with JavaScript
description: Learn how to create an interactive temperature converter that converts between Celsius, Fahrenheit, and Kelvin using JavaScript.
keywords:
- 'temperature converter'
- 'javascript project'
- 'unit conversion'
- 'html and css'
roadmapIds:
roadmapIds:
- 'frontend'
---
@ -30,4 +30,4 @@ Here is a mockup of what the temperature converter might look like:
[![Temperature Converter](https://assets.roadmap.sh/guest/temperature-converter-8omel.png)](https://assets.roadmap.sh/guest/temperature-converter-8omel.png)
This project will help you gain experience with handling user input, conditionally enabling form elements, and performing simple calculations using JavaScript.
This project will help you gain experience with handling user input, conditionally enabling form elements, and performing simple calculations using JavaScript.

@ -1,54 +1,17 @@
import type { MarkdownFileType } from './file';
import { getCollection, type CollectionEntry } from 'astro:content';
import { getRoadmapById, type RoadmapFileType } from './roadmap.ts';
export const projectDifficulties = [
'beginner',
'intermediate',
'advanced',
] as const;
export type ProjectDifficultyType = (typeof projectDifficulties)[number];
export interface ProjectFrontmatter {
title: string;
description: string;
isNew: boolean;
sort: number;
difficulty: ProjectDifficultyType;
nature: string;
skills: string[];
seo: {
title: string;
description: string;
keywords: string[];
ogImageUrl: string;
};
roadmapIds: string[];
}
export type ProjectFileType = MarkdownFileType<ProjectFrontmatter> & {
id: string;
export type ProjectFileType = CollectionEntry<'projects'> & {
roadmaps: RoadmapFileType[];
};
/**
* Generates id from the given project file
* @param filePath Markdown file path
*
* @returns unique project identifier
*/
function projectPathToId(filePath: string): string {
const fileName = filePath.split('/').pop() || '';
return fileName.replace('.md', '');
}
export async function getProjectsByRoadmapId(
roadmapId: string,
): Promise<ProjectFileType[]> {
const projects = await getAllProjects();
return projects.filter((project) =>
project.frontmatter?.roadmapIds?.includes(roadmapId),
project.data?.roadmapIds?.includes(roadmapId),
);
}
@ -63,26 +26,20 @@ export async function getAllProjects(): Promise<ProjectFileType[]> {
return tempProjects;
}
const projects = import.meta.glob<ProjectFileType>(
'/src/data/projects/*.md',
{
eager: true,
},
);
tempProjects = Object.values(projects).map((projectFile) => ({
...projectFile,
id: projectPathToId(projectFile.file),
}));
tempProjects = await getCollection('projects');
return tempProjects;
}
export async function getProjectById(
groupId: string,
): Promise<ProjectFileType> {
const project = await import(`../data/projects/${groupId}.md`);
const roadmapIds = project.frontmatter.roadmapIds || [];
const projects = await getAllProjects();
const project = projects.find((project) => project.slug === groupId);
if (!project) {
throw new Error(`Project not found with id: ${groupId}`);
}
const roadmapIds = project.data.roadmapIds || [];
const roadmaps = await Promise.all(
roadmapIds.map((roadmapId: string) => getRoadmapById(roadmapId)),
);
@ -90,7 +47,6 @@ export async function getProjectById(
return {
...project,
roadmaps: roadmaps,
id: projectPathToId(project.file),
};
}
@ -101,7 +57,7 @@ export async function getRoadmapsProjects(): Promise<
const roadmapsProjects: Record<string, ProjectFileType[]> = {};
projects.forEach((project) => {
project.frontmatter.roadmapIds.forEach((roadmapId) => {
project.data.roadmapIds.forEach((roadmapId) => {
if (!roadmapsProjects[roadmapId]) {
roadmapsProjects[roadmapId] = [];
}

@ -44,7 +44,7 @@ export async function getVideosByAuthor(
): Promise<VideoFileType[]> {
const allVideos = await getAllVideos();
return allVideos.filter((video) => video.author?.id === authorId);
return allVideos.filter((video) => video.author?.slug === authorId);
}
/**
@ -63,7 +63,7 @@ export async function getAllVideos(): Promise<VideoFileType[]> {
...videoFile,
id: videoPathToId(videoFile.file),
author: allAuthors.find(
(author) => author.id === videoFile.frontmatter.authorId,
(author) => author.slug === videoFile.frontmatter.authorId,
)!,
}));

@ -52,7 +52,7 @@ const nounTitle =
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);
const projectIds = projects.map((project) => project.id);
const projectIds = projects.map((project) => project.slug);
const projectApiClient = projectApi(Astro);
const { response: userCounts } =

@ -32,7 +32,7 @@ const videos = await getVideosByAuthor(authorId);
---
<BaseLayout
permalink={`/authors/${author.id}`}
permalink={`/authors/${author.slug}`}
title={`${authorFrontmatter.name} - Author at roadmap.sh`}
briefTitle={authorFrontmatter.name}
ogImageUrl={`https://roadmap.sh/${authorFrontmatter.imageUrl}`}

@ -93,7 +93,7 @@ const videos = await getAllVideos();
allowBookmark={false}
featuredItems={questionGroups.map((questionGroup) => ({
text: questionGroup.data.briefTitle,
url: `/questions/${questionGroup.id}`,
url: `/questions/${questionGroup.slug}`,
isNew: questionGroup.data.isNew,
}))}
/>

@ -34,16 +34,16 @@ export async function GET() {
group: 'Best Practices',
})),
...questionGroups.map((questionGroup) => ({
id: questionGroup.id,
url: `/questions/${questionGroup.id}`,
id: questionGroup.slug,
url: `/questions/${questionGroup.slug}`,
title: questionGroup.data.briefTitle,
group: 'Questions',
})),
...guides.map((guide) => ({
id: guide.id,
id: guide.slug,
url: guide.data.excludedBySlug
? guide.data.excludedBySlug
: `/guides/${guide.id}`,
: `/guides/${guide.slug}`,
title: guide.data.title,
description: guide.data.description,
authorId: guide.data.authorId,
@ -56,10 +56,10 @@ export async function GET() {
group: 'Videos',
})),
...projects.map((project) => ({
id: project.id,
url: `/projects/${project.id}`,
title: project.frontmatter.title,
description: project.frontmatter.description,
id: project.slug,
url: `/projects/${project.slug}`,
title: project.data.title,
description: project.data.description,
group: 'Projects',
})),
]),

@ -1,11 +1,7 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Badge } from '../../../components/Badge';
import {
getAllProjects,
getProjectById,
type ProjectFrontmatter,
} from '../../../lib/project';
import { getAllProjects, getProjectById } from '../../../lib/project';
import AstroIcon from '../../../components/AstroIcon.astro';
import { ProjectStepper } from '../../../components/Projects/StatusStepper/ProjectStepper';
import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
@ -14,7 +10,7 @@ export async function getStaticPaths() {
const projects = await getAllProjects();
return projects
.map((project) => project.id)
.map((project) => project.slug)
.map((projectId) => ({
params: { projectId },
}));
@ -27,7 +23,8 @@ interface Params extends Record<string, string | undefined> {
const { projectId } = Astro.params as Params;
const project = await getProjectById(projectId);
const projectData = project.frontmatter as ProjectFrontmatter;
const projectData = project.data;
const { Content } = await project.render();
let jsonLdSchema: any[] = [];
@ -49,7 +46,11 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
>
<div class='bg-gray-50'>
<div class='container'>
<ProjectTabs parentRoadmapId={parentRoadmapId} projectId={projectId} activeTab='details' />
<ProjectTabs
parentRoadmapId={parentRoadmapId}
projectId={projectId}
activeTab='details'
/>
<div
class='mb-4 rounded-lg border bg-gradient-to-b from-gray-100 to-white to-10% p-4 py-2 sm:p-5'
@ -80,7 +81,7 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
<div
class='prose max-w-full prose-h2:mb-3 prose-h2:mt-5 prose-h3:mb-1 prose-h3:mt-5 prose-p:mb-2 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-pre:my-3 prose-ul:my-3.5 prose-hr:my-5 [&>ul>li]:my-1'
>
<project.Content />
<Content />
</div>
<div

@ -1,10 +1,6 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import {
getAllProjects,
getProjectById,
type ProjectFrontmatter,
} from '../../../lib/project';
import { getAllProjects, getProjectById } from '../../../lib/project';
import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
import { ListProjectSolutions } from '../../../components/Projects/ListProjectSolutions';
import { ProjectSolutionModal } from '../../../components/Projects/ProjectSolutionModal';
@ -13,7 +9,7 @@ export async function getStaticPaths() {
const projects = await getAllProjects();
return projects
.map((project) => project.id)
.map((project) => project.slug)
.map((projectId) => ({
params: { projectId },
}));
@ -26,7 +22,7 @@ interface Params extends Record<string, string | undefined> {
const { projectId } = Astro.params as Params;
const project = await getProjectById(projectId);
const projectData = project.frontmatter as ProjectFrontmatter;
const projectData = project.data;
let jsonLdSchema: any[] = [];

@ -12,7 +12,7 @@ const allRoadmapIds = Object.keys(roadmapProjects);
const allRoadmaps = await getRoadmapsByIds(allRoadmapIds);
const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
const projects = (roadmapProjects[roadmap.id] || []).sort((a, b) => {
return a.frontmatter.sort - b.frontmatter.sort;
return a.data.sort - b.data.sort;
});
return {
@ -25,7 +25,7 @@ const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
const projectIds = allRoadmapIds
.map((id) => roadmapProjects[id])
.flat()
.map((project) => project.id);
.map((project) => project.slug);
const projectApiClient = projectApi(Astro);
const { response: userCounts } =
await projectApiClient.listProjectsUserCount(projectIds);

@ -34,7 +34,7 @@ const { data: frontmatter } = questionGroup;
briefTitle={frontmatter.briefTitle}
description={frontmatter.seo.description}
keywords={frontmatter.seo.keywords}
permalink={`/questions/${questionGroup.id}`}
permalink={`/questions/${questionGroup.slug}`}
>
{
!frontmatter.authorId && (

Loading…
Cancel
Save