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 && ( {hideRenderer && (
<EmptyRoadmap <EmptyRoadmap
roadmapId={roadmapId} roadmapId={roadmapId}
canManage={roadmap.canManage} // @ts-ignore
canManage={roadmap?.canManage}
className="grow" className="grow"
/> />
)} )}

@ -40,7 +40,7 @@ const { data: guideFrontmatter, author } = guide;
</h1> </h1>
<p class='my-0 flex items-center justify-start text-sm text-gray-400'> <p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a <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' class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
> >
<img <img
@ -53,7 +53,7 @@ const { data: guideFrontmatter, author } = guide;
<span class='mx-2 hidden sm:inline'>&middot;</span> <span class='mx-2 hidden sm:inline'>&middot;</span>
<a <a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline' 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' target='_blank'
> >
Improve this Guide Improve this Guide

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

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

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

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

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

@ -80,7 +80,7 @@ const { data: guideFrontmatter, author } = questionGroup;
author && ( author && (
<p class='my-0 flex items-center justify-start text-sm text-gray-400'> <p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a <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' class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
> >
<img <img
@ -93,7 +93,7 @@ const { data: guideFrontmatter, author } = questionGroup;
<span class='mx-2 hidden sm:inline'>&middot;</span> <span class='mx-2 hidden sm:inline'>&middot;</span>
<a <a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline' 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' target='_blank'
> >
Improve this Guide Improve this Guide
@ -110,7 +110,7 @@ const { data: guideFrontmatter, author } = questionGroup;
</p> </p>
<div class='mx-0 sm:-mb-32'> <div class='mx-0 sm:-mb-32'>
<QuestionsList <QuestionsList
groupId={questionGroup.id} groupId={questionGroup.slug}
questions={questionGroup.questions} questions={questionGroup.questions}
client:load client:load
/> />

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

@ -1,9 +1,11 @@
import { authorCollection } from './author'; import { authorCollection } from './author';
import { guideCollection } from './guide'; import { guideCollection } from './guide';
import { projectCollection } from './project';
import { questionGroupCollection } from './question-group'; import { questionGroupCollection } from './question-group';
export const collections = { export const collections = {
authors: authorCollection, authors: authorCollection,
guides: guideCollection, guides: guideCollection,
'question-groups': questionGroupCollection, '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 - JavaScript
- DOM Manipulation - DOM Manipulation
seo: seo:
- title: Build a Temperature Converter with JavaScript 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. description: Learn how to create an interactive temperature converter that converts between Celsius, Fahrenheit, and Kelvin using JavaScript.
- keywords: keywords:
- 'temperature converter' - 'temperature converter'
- 'javascript project' - 'javascript project'
- 'unit conversion' - 'unit conversion'
- 'html and css' - 'html and css'
roadmapIds: roadmapIds:
- 'frontend' - '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) [![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'; import { getRoadmapById, type RoadmapFileType } from './roadmap.ts';
export const projectDifficulties = [ export type ProjectFileType = CollectionEntry<'projects'> & {
'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;
roadmaps: RoadmapFileType[]; 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( export async function getProjectsByRoadmapId(
roadmapId: string, roadmapId: string,
): Promise<ProjectFileType[]> { ): Promise<ProjectFileType[]> {
const projects = await getAllProjects(); const projects = await getAllProjects();
return projects.filter((project) => 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; return tempProjects;
} }
const projects = import.meta.glob<ProjectFileType>( tempProjects = await getCollection('projects');
'/src/data/projects/*.md',
{
eager: true,
},
);
tempProjects = Object.values(projects).map((projectFile) => ({
...projectFile,
id: projectPathToId(projectFile.file),
}));
return tempProjects; return tempProjects;
} }
export async function getProjectById( export async function getProjectById(
groupId: string, groupId: string,
): Promise<ProjectFileType> { ): Promise<ProjectFileType> {
const project = await import(`../data/projects/${groupId}.md`); const projects = await getAllProjects();
const roadmapIds = project.frontmatter.roadmapIds || []; 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( const roadmaps = await Promise.all(
roadmapIds.map((roadmapId: string) => getRoadmapById(roadmapId)), roadmapIds.map((roadmapId: string) => getRoadmapById(roadmapId)),
); );
@ -90,7 +47,6 @@ export async function getProjectById(
return { return {
...project, ...project,
roadmaps: roadmaps, roadmaps: roadmaps,
id: projectPathToId(project.file),
}; };
} }
@ -101,7 +57,7 @@ export async function getRoadmapsProjects(): Promise<
const roadmapsProjects: Record<string, ProjectFileType[]> = {}; const roadmapsProjects: Record<string, ProjectFileType[]> = {};
projects.forEach((project) => { projects.forEach((project) => {
project.frontmatter.roadmapIds.forEach((roadmapId) => { project.data.roadmapIds.forEach((roadmapId) => {
if (!roadmapsProjects[roadmapId]) { if (!roadmapsProjects[roadmapId]) {
roadmapsProjects[roadmapId] = []; roadmapsProjects[roadmapId] = [];
} }

@ -44,7 +44,7 @@ export async function getVideosByAuthor(
): Promise<VideoFileType[]> { ): Promise<VideoFileType[]> {
const allVideos = await getAllVideos(); 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, ...videoFile,
id: videoPathToId(videoFile.file), id: videoPathToId(videoFile.file),
author: allAuthors.find( 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 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 projects = await getProjectsByRoadmapId(roadmapId);
const projectIds = projects.map((project) => project.id); const projectIds = projects.map((project) => project.slug);
const projectApiClient = projectApi(Astro); const projectApiClient = projectApi(Astro);
const { response: userCounts } = const { response: userCounts } =

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

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

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

@ -1,11 +1,7 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Badge } from '../../../components/Badge'; import { Badge } from '../../../components/Badge';
import { import { getAllProjects, getProjectById } from '../../../lib/project';
getAllProjects,
getProjectById,
type ProjectFrontmatter,
} from '../../../lib/project';
import AstroIcon from '../../../components/AstroIcon.astro'; import AstroIcon from '../../../components/AstroIcon.astro';
import { ProjectStepper } from '../../../components/Projects/StatusStepper/ProjectStepper'; import { ProjectStepper } from '../../../components/Projects/StatusStepper/ProjectStepper';
import { ProjectTabs } from '../../../components/Projects/ProjectTabs'; import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
@ -14,7 +10,7 @@ export async function getStaticPaths() {
const projects = await getAllProjects(); const projects = await getAllProjects();
return projects return projects
.map((project) => project.id) .map((project) => project.slug)
.map((projectId) => ({ .map((projectId) => ({
params: { projectId }, params: { projectId },
})); }));
@ -27,7 +23,8 @@ interface Params extends Record<string, string | undefined> {
const { projectId } = Astro.params as Params; const { projectId } = Astro.params as Params;
const project = await getProjectById(projectId); const project = await getProjectById(projectId);
const projectData = project.frontmatter as ProjectFrontmatter; const projectData = project.data;
const { Content } = await project.render();
let jsonLdSchema: any[] = []; let jsonLdSchema: any[] = [];
@ -49,7 +46,11 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
> >
<div class='bg-gray-50'> <div class='bg-gray-50'>
<div class='container'> <div class='container'>
<ProjectTabs parentRoadmapId={parentRoadmapId} projectId={projectId} activeTab='details' /> <ProjectTabs
parentRoadmapId={parentRoadmapId}
projectId={projectId}
activeTab='details'
/>
<div <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' 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 <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' 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>
<div <div

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

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

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

Loading…
Cancel
Save