diff --git a/astro.config.mjs b/astro.config.mjs index 88abd3b7a..f4d152ab6 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -41,18 +41,11 @@ export default defineConfig({ ], ], }, - // @FIXME: - // This should be "hybrid" but there is a bug in the current version of Astro - // that adds trailing slashes to the URLs when using "hybrid" mode. - // ---------------------------------------------- - // https://github.com/withastro/astro/issues/7808 - // ---------------------------------------------- - // For now, we are using "server" mode and then using cloudfront to cache the - // pages and serve them as static. - output: 'server', + output: 'hybrid', adapter: node({ mode: 'standalone', }), + trailingSlash: 'never', integrations: [ tailwind({ config: { diff --git a/src/pages/[roadmapId]/[...topicId].astro b/src/pages/[roadmapId]/[...topicId].astro index 398a0b3ec..aec201c85 100644 --- a/src/pages/[roadmapId]/[...topicId].astro +++ b/src/pages/[roadmapId]/[...topicId].astro @@ -1,22 +1,32 @@ --- -import { getRoadmapTopicFiles } from '../../lib/roadmap-topic'; - -export const partial = true; - -const { topicId, roadmapId } = Astro.params; -if (!topicId) { - return new Response(); +import { + getRoadmapTopicFiles, + type RoadmapTopicFileType, +} from '../../lib/roadmap-topic'; + +export async function getStaticPaths() { + const topicPathMapping = await getRoadmapTopicFiles(); + + return Object.keys(topicPathMapping).map((topicSlug) => { + const topicDetails = topicPathMapping[topicSlug]; + const roadmapId = topicDetails.roadmapId; + const topicId = topicSlug.replace(`/${roadmapId}/`, ''); + + return { + params: { + topicId, + roadmapId, + }, + props: topicDetails, + }; + }); } -const topicSlug = `/${roadmapId}/${topicId}`; -const topicPathMapping = await getRoadmapTopicFiles(); - -const topicDetails = topicPathMapping[topicSlug]; -if (!topicDetails) { - return Astro.redirect('/404'); -} +export const partial = true; -const { file } = topicDetails; +const { topicId } = Astro.params; +const { file, url, roadmapId, roadmap, heading } = + Astro.props as RoadmapTopicFileType; const fileWithoutBasePath = file.file?.replace(/.+?\/src\/data/, '/src/data'); const gitHubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master${fileWithoutBasePath}`; diff --git a/src/pages/[roadmapId]/index.astro b/src/pages/[roadmapId]/index.astro index 2a71c2251..898c6d161 100644 --- a/src/pages/[roadmapId]/index.astro +++ b/src/pages/[roadmapId]/index.astro @@ -13,25 +13,27 @@ import { generateFAQSchema, } from '../../lib/jsonld-schema'; import { getOpenGraphImageUrl } from '../../lib/open-graph'; +import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; -import { - getRoadmapById, - type RoadmapFrontmatter, - getRoadmapFaqsById, -} from '../../lib/roadmap'; +export async function getStaticPaths() { + const roadmapIds = await getRoadmapIds(); + + return roadmapIds.map((roadmapId) => ({ + params: { roadmapId }, + })); +} interface Params extends Record { roadmapId: string; } const { roadmapId } = Astro.params as Params; - -const roadmapFile = await getRoadmapById(roadmapId).catch(() => null); -if (!roadmapFile) { - return Astro.redirect('/404'); -} - -const roadmapFAQs = await getRoadmapFaqsById(roadmapId); +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; let jsonLdSchema = []; diff --git a/src/pages/[roadmapId]/index.json.ts b/src/pages/[roadmapId]/index.json.ts index 391f39930..7d4b6c0f3 100644 --- a/src/pages/[roadmapId]/index.json.ts +++ b/src/pages/[roadmapId]/index.json.ts @@ -1,59 +1,30 @@ import type { APIRoute } from 'astro'; -export const GET: APIRoute = async function ({ params, url, request, props }) { - const { roadmapId: fullRoadmapId } = params; - if (!fullRoadmapId) { - return new Response( - JSON.stringify({ - data: null, - error: { - message: 'Roadmap not found', - }, - }), - { - status: 500, - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - } - - // to account for `roadmap/roadmap-beginner.json` files - const roadmapId = - fullRoadmapId?.indexOf('-beginner') !== -1 - ? fullRoadmapId.replace('-beginner', '') - : fullRoadmapId; +export async function getStaticPaths() { + const roadmapJsons = import.meta.glob('/src/data/roadmaps/**/*.json', { + eager: true, + }); - const fileName = - roadmapId === fullRoadmapId ? `${roadmapId}.json` : `${fullRoadmapId}.json`; + return Object.keys(roadmapJsons).map((filePath) => { + const roadmapId = filePath.split('/').pop()?.replace('.json', ''); + const roadmapJson = roadmapJsons[filePath] as Record; - try { - const roadmapJson = await import( - /* @vite-ignore */ `../../data/roadmaps/${roadmapId}/${fileName}` - ); - - return new Response(JSON.stringify(roadmapJson), { - status: 200, - headers: { - 'Content-Type': 'application/json', + return { + params: { + roadmapId, }, - }); - } catch (error) { - return new Response( - JSON.stringify({ - data: null, - error: { - message: 'Roadmap not found', - detail: (error as any).message, - }, - }), - { - status: 500, - headers: { - 'Content-Type': 'application/json', - }, + props: { + roadmapJson: roadmapJson?.default, }, - ); - } + }; + }); +} + +export const GET: APIRoute = async function ({ params, request, props }) { + return new Response(JSON.stringify(props.roadmapJson), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); }; diff --git a/src/pages/authors/[authorId].astro b/src/pages/authors/[authorId].astro index ae01a9d6e..1d0cf6138 100644 --- a/src/pages/authors/[authorId].astro +++ b/src/pages/authors/[authorId].astro @@ -4,21 +4,22 @@ import AstroIcon from '../../components/AstroIcon.astro'; import { getGuidesByAuthor } from '../../lib/guide'; import { getVideosByAuthor } from '../../lib/video'; import GuideListItem from '../../components/GuideListItem.astro'; -import { getAuthorById } from '../../lib/author'; +import { getAuthorById, getAuthorIds } from '../../lib/author'; import VideoListItem from '../../components/VideoListItem.astro'; interface Params extends Record {} -const { authorId } = Astro.params; -if (!authorId) { - return Astro.redirect('/404'); -} +export async function getStaticPaths() { + const authorIds = await getAuthorIds(); -const author = await getAuthorById(authorId); -if (!author) { - return Astro.redirect('/404'); + return authorIds.map((authorId) => ({ + params: { authorId }, + })); } +const { authorId } = Astro.params; + +const author = await getAuthorById(authorId); const authorFrontmatter = author.frontmatter; const guides = await getGuidesByAuthor(authorId); diff --git a/src/pages/authors/[authorId].json.ts b/src/pages/authors/[authorId].json.ts index 24851c7e7..4193292f7 100644 --- a/src/pages/authors/[authorId].json.ts +++ b/src/pages/authors/[authorId].json.ts @@ -1,20 +1,25 @@ import type { APIRoute } from 'astro'; -import { getAuthorById } from '../../lib/author'; +import { getAuthorById, getAuthorIds } from '../../lib/author'; -export const GET: APIRoute = async function ({ params, request, props }) { - const { authorId } = params as { authorId: string }; +export async function getStaticPaths() { + const authorIds = await getAuthorIds(); + + return await Promise.all( + authorIds.map(async (authorId) => { + const authorDetails = await getAuthorById(authorId); - const authorDetails = await getAuthorById(authorId); - if (!authorDetails) { - return new Response(JSON.stringify({ error: 'Not found' }), { - status: 404, - headers: { - 'Content-Type': 'application/json', - }, - }); - } + return { + params: { authorId }, + props: { + authorDetails: authorDetails?.frontmatter || {}, + }, + }; + }), + ); +} - return new Response(JSON.stringify(authorDetails?.frontmatter), { +export const GET: APIRoute = async function ({ params, request, props }) { + return new Response(JSON.stringify(props.authorDetails), { status: 200, headers: { 'Content-Type': 'application/json', diff --git a/src/pages/best-practices/[bestPracticeId]/[...topicId].astro b/src/pages/best-practices/[bestPracticeId]/[...topicId].astro index e1c874932..1607036b0 100644 --- a/src/pages/best-practices/[bestPracticeId]/[...topicId].astro +++ b/src/pages/best-practices/[bestPracticeId]/[...topicId].astro @@ -1,16 +1,26 @@ --- import { getAllBestPracticeTopicFiles } from '../../../lib/best-practice-topic'; +import type { BestPracticeTopicFileType } from '../../../lib/best-practice-topic'; -const { topicId, bestPracticeId } = Astro.params; -if (!topicId) { - return new Response(); -} +export async function getStaticPaths() { + const topicPathMapping = await getAllBestPracticeTopicFiles(); + + return Object.keys(topicPathMapping).map((topicSlug) => { + const topicDetails = topicPathMapping[topicSlug]; + const bestPracticeId = topicDetails.bestPracticeId; + const topicId = topicSlug.replace(`/${bestPracticeId}/`, ''); -const topicSlug = `/${bestPracticeId}/${topicId}`; -const topicPathMapping = await getAllBestPracticeTopicFiles(); + return { + params: { + topicId, + bestPracticeId, + }, + props: topicDetails, + }; + }); +} -const topicDetails = topicPathMapping[topicSlug]; -const { file } = topicDetails; +const { file } = Astro.props as BestPracticeTopicFileType; const fileWithoutBasePath = file.file?.replace(/.+?\/src\/data/, '/src/data'); const gitHubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master${fileWithoutBasePath}`; diff --git a/src/pages/best-practices/[bestPracticeId]/index.astro b/src/pages/best-practices/[bestPracticeId]/index.astro index 71f482bac..54a1fd744 100644 --- a/src/pages/best-practices/[bestPracticeId]/index.astro +++ b/src/pages/best-practices/[bestPracticeId]/index.astro @@ -7,13 +7,24 @@ import { TopicDetail } from '../../../components/TopicDetail/TopicDetail'; import UpcomingForm from '../../../components/UpcomingForm.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro'; import { UserProgressModal } from '../../../components/UserProgress/UserProgressModal'; -import { - type BestPracticeFileType, - type BestPracticeFrontmatter, - getBestPracticeById, -} from '../../../lib/best-practice'; import { generateArticleSchema } from '../../../lib/jsonld-schema'; import { getOpenGraphImageUrl } from '../../../lib/open-graph'; +import { + BestPracticeFileType, + BestPracticeFrontmatter, + getAllBestPractices, +} from '../../../lib/best-practice'; + +export async function getStaticPaths() { + const bestPractices = await getAllBestPractices(); + + return bestPractices.map((bestPractice: BestPracticeFileType) => ({ + params: { bestPracticeId: bestPractice.id }, + props: { + bestPractice: bestPractice, + }, + })); +} interface Params extends Record { bestPracticeId: string; @@ -24,14 +35,7 @@ interface Props { } const { bestPracticeId } = Astro.params as Params; -const bestPractice = await getBestPracticeById(bestPracticeId).catch( - () => null, -); - -if (!bestPractice) { - return Astro.redirect('/404'); -} - +const { bestPractice } = Astro.props as Props; const bestPracticeData = bestPractice.frontmatter as BestPracticeFrontmatter; let jsonLdSchema = []; diff --git a/src/pages/best-practices/[bestPracticeId]/index.json.ts b/src/pages/best-practices/[bestPracticeId]/index.json.ts index 84d50ef31..00400ce13 100644 --- a/src/pages/best-practices/[bestPracticeId]/index.json.ts +++ b/src/pages/best-practices/[bestPracticeId]/index.json.ts @@ -1,33 +1,33 @@ import type { APIRoute } from 'astro'; -export const GET: APIRoute = async function ({ params, request, props }) { - const { bestPracticeId } = params; +export async function getStaticPaths() { + const bestPracticeJsons = await import.meta.glob( + '/src/data/best-practices/**/*.json', + { + eager: true, + }, + ); - try { - const roadmapJson = await import( - `../../../data/best-practices/${bestPracticeId}/${bestPracticeId}.json` - ); + return Object.keys(bestPracticeJsons).map((filePath) => { + const bestPracticeId = filePath.split('/').pop()?.replace('.json', ''); + const bestPracticeJson = bestPracticeJsons[filePath] as Record; - return new Response(JSON.stringify(roadmapJson), { - status: 200, - headers: { - 'Content-Type': 'application/json', + return { + params: { + bestPracticeId, }, - }); - } catch (error) { - return new Response( - JSON.stringify({ - data: null, - error: { - message: 'Best Practices not found', - }, - }), - { - status: 500, - headers: { - 'Content-Type': 'application/json', - }, + props: { + bestPracticeJson: bestPracticeJson?.default, }, - ); - } + }; + }); +} + +export const GET: APIRoute = async function ({ params, request, props }) { + return new Response(JSON.stringify(props.bestPracticeJson), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); }; diff --git a/src/pages/g/[linkGroupId]/[linkId].astro b/src/pages/g/[linkGroupId]/[linkId].astro index 30dc02082..a63e52f4a 100644 --- a/src/pages/g/[linkGroupId]/[linkId].astro +++ b/src/pages/g/[linkGroupId]/[linkId].astro @@ -1,14 +1,31 @@ --- import BaseLayout from '../../../layouts/BaseLayout.astro'; import SkeletonLayout from '../../../layouts/SkeletonLayout.astro'; -import { getLinkGroupById } from '../../../lib/link-group'; +import { getAllLinkGroups } from '../../../lib/link-group'; -const { linkId } = Astro.params; -const linkGroup = await getLinkGroupById(linkId!).catch(() => null); -if (!linkGroup) { - return Astro.redirect('/404'); +export async function getStaticPaths() { + const linkGroups = await getAllLinkGroups(); + + return linkGroups.flatMap((linkGroup) => { + const linkGroupLinks = linkGroup.frontmatter; + + return Object.keys(linkGroupLinks).map((slug) => { + return { + params: { + linkGroupId: linkGroup.id, + linkId: slug, + }, + props: { + linkGroup, + }, + }; + }); + }); } +const { linkId } = Astro.params; +const { linkGroup } = Astro.props; + const fullUrl = linkGroup.frontmatter[linkId!]; --- diff --git a/src/pages/guides/[guideId].astro b/src/pages/guides/[guideId].astro index 730213a95..db5956d7d 100644 --- a/src/pages/guides/[guideId].astro +++ b/src/pages/guides/[guideId].astro @@ -2,23 +2,27 @@ import GuideContent from '../../components/Guide/GuideContent.astro'; import GuideHeader from '../../components/GuideHeader.astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getGuideById } from '../../lib/guide'; +import { getAllGuides, type GuideFileType } from '../../lib/guide'; import { getOpenGraphImageUrl } from '../../lib/open-graph'; -interface Params extends Record { - guideId: string; +export interface Props { + guide: GuideFileType; } -const { guideId } = Astro.params; -if (!guideId) { - return Astro.redirect('/404'); -} -const guide = await getGuideById(guideId!).catch(() => null); -if (!guide) { - return Astro.redirect('/404'); +export async function getStaticPaths() { + const guides = (await getAllGuides()).filter( + (guide) => !guide.frontmatter.excludedBySlug, + ); + + return guides.map((guide) => ({ + params: { guideId: guide.id }, + props: { guide }, + })); } -const { frontmatter: guideData } = guide; +const { guideId } = Astro.params; +const { guide } = Astro.props; +const { frontmatter: guideData, author } = guide; const ogImageUrl = getOpenGraphImageUrl({ group: 'guides', diff --git a/src/pages/questions/[questionGroupId].astro b/src/pages/questions/[questionGroupId].astro index 666c84458..1f72d1a46 100644 --- a/src/pages/questions/[questionGroupId].astro +++ b/src/pages/questions/[questionGroupId].astro @@ -6,16 +6,26 @@ import Footer from '../../components/Footer.astro'; import AstroIcon from '../../components/AstroIcon.astro'; import { QuestionsList } from '../../components/Questions/QuestionsList'; -import { getQuestionGroupById } from '../../lib/question-group'; +import { + getAllQuestionGroups, + type QuestionGroupType, +} from '../../lib/question-group'; -const { questionGroupId } = Astro.params; -const questionGroup = await getQuestionGroupById(questionGroupId!).catch( - () => null, -); -if (!questionGroup) { - return Astro.redirect('/404'); +export interface Props { + questionGroup: QuestionGroupType; } +export async function getStaticPaths() { + const questionGroups = await getAllQuestionGroups(); + return questionGroups.map((questionGroup) => { + return { + params: { questionGroupId: questionGroup.id }, + props: { questionGroup }, + }; + }); +} + +const { questionGroup } = Astro.props; const { frontmatter } = questionGroup; --- diff --git a/src/pages/r/[customRoadmapSlug].astro b/src/pages/r/[customRoadmapSlug].astro index aa18e01de..e72b34c9f 100644 --- a/src/pages/r/[customRoadmapSlug].astro +++ b/src/pages/r/[customRoadmapSlug].astro @@ -5,6 +5,8 @@ import { SkeletonRoadmapHeader } from '../../components/CustomRoadmap/SkeletonRo import Loader from '../../components/Loader.astro'; import ProgressHelpPopup from '../../components/ProgressHelpPopup.astro'; +export const prerender = false; + const { customRoadmapSlug } = Astro.params; --- diff --git a/src/pages/u/[username]/index.astro b/src/pages/u/[username].astro similarity index 79% rename from src/pages/u/[username]/index.astro rename to src/pages/u/[username].astro index 843c4bb73..ef0a83d03 100644 --- a/src/pages/u/[username]/index.astro +++ b/src/pages/u/[username].astro @@ -1,10 +1,12 @@ --- import { FrownIcon } from 'lucide-react'; -import { userApi } from '../../../api/user'; -import AccountLayout from '../../../layouts/AccountLayout.astro'; -import { UserPublicProfilePage } from '../../../components/UserPublicProfile/UserPublicProfilePage'; -import OpenSourceBanner from '../../../components/OpenSourceBanner.astro'; -import Footer from '../../../components/Footer.astro'; +import { userApi } from '../../api/user'; +import AccountLayout from '../../layouts/AccountLayout.astro'; +import { UserPublicProfilePage } from '../../components/UserPublicProfile/UserPublicProfilePage'; +import OpenSourceBanner from '../../components/OpenSourceBanner.astro'; +import Footer from '../../components/Footer.astro'; + +export const prerender = false; interface Params extends Record { username: string; diff --git a/src/pages/v1-stats.json.ts b/src/pages/v1-stats.json.ts index 4986d75a4..681fe6036 100644 --- a/src/pages/v1-stats.json.ts +++ b/src/pages/v1-stats.json.ts @@ -1,7 +1,5 @@ import { execSync } from 'child_process'; -export const prerender = true; - export async function GET() { const commitHash = execSync('git rev-parse HEAD').toString().trim(); const commitDate = execSync('git log -1 --format=%cd').toString().trim(); diff --git a/src/pages/videos/[videoId].astro b/src/pages/videos/[videoId].astro index 3133d733c..096e230a0 100644 --- a/src/pages/videos/[videoId].astro +++ b/src/pages/videos/[videoId].astro @@ -1,14 +1,23 @@ --- import VideoHeader from '../../components/VideoHeader.astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getVideoById } from '../../lib/video'; +import { getAllVideos, VideoFileType } from '../../lib/video'; -const { videoId } = Astro.params; +export interface Props { + video: VideoFileType; +} + +export async function getStaticPaths() { + const videos = await getAllVideos(); -const video = await getVideoById(videoId).catch(() => null); -if (!video) { - return Astro.redirect('/404'); + return videos.map((video) => ({ + params: { videoId: video.id }, + props: { video }, + })); } + +const { videoId } = Astro.params; +const { video } = Astro.props; ---