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. 6
      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,9 +11,9 @@ 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'

@ -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